diff --git a/.gitignore b/.gitignore index f31dc361ac..760d354556 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ build/target build/solr build/solr_index build/indexer_state +build/indexer_pui_state build/archivesspace_demo_db build/*.tmp build/.gem @@ -41,3 +42,5 @@ Gemfile.local supervisord.* mysql-connector*.jar common/config/config.rb +public-new/vendor/assets/stylesheets/largetree.less +public-new/vendor/assets/javascripts/largetree.js.erb diff --git a/.travis.yml b/.travis.yml index 9715e2db31..73e847e18a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,10 +22,11 @@ env: - TASK=dist before_install: - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile - --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 2560x1700x24 + --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 2560x10240x24 - export DISPLAY=:99.0 before_script: -- (cd /tmp; wget 'https://ftp.mozilla.org/pub/firefox/releases/47.0.1/linux-x86_64/en-US/firefox-47.0.1.tar.bz2'; tar xvjf firefox-47.0.1.tar.bz2) +- free -m +- (cd /tmp; wget 'https://ftp.mozilla.org/pub/firefox/releases/52.0/linux-x86_64/en-US/firefox-52.0.tar.bz2'; tar xvjf firefox-52.0.tar.bz2) - sleep 3 - if [[ "$DB" == "mysql" ]]; then (mkdir lib; cd lib; curl -Oq http://central.maven.org/maven2/mysql/mysql-connector-java/5.1.39/mysql-connector-java-5.1.39.jar ); fi @@ -33,7 +34,7 @@ before_script: fi - if [[ "$DB" == "mysql" ]]; then mysql -e "create database archivesspace default character set utf8;"; fi -- 'export JAVA_OPTS="-Xmx1G $JAVA_OPTS"' +- 'export JAVA_OPTS="-Xmx512m -verbose:gc $JAVA_OPTS"' - 'export PATH="/tmp/firefox:$PATH"' - firefox --version branches: @@ -44,6 +45,7 @@ script: notifications: irc: irc.freenode.org#archivesspace email: false +bundler_args: --retry 5 addons: artifacts: debug: true @@ -54,4 +56,7 @@ addons: - $( ls /var/tmp/*.html | tr "\n" ":" ) - $( ls /var/log/mysql/* | tr "\n" ":" ) - $( ls /var/tmp/aspace-integration.log | tr "\n" ":" ) -sudo: false +sudo: true +after_failure: +- if [[ -e "/tmp/firefox_console" ]]; then cat /tmp/firefox_console; fi +- dmesg | tail -500 diff --git a/README.md b/README.md index b5420fbc06..f0849fa514 100644 --- a/README.md +++ b/README.md @@ -39,13 +39,6 @@ You can check your Java version by running the command: java -version - - When you extract the `.zip` file, it will create a directory called `archivesspace`. To run the system, just execute the appropriate startup script for your platform. On Linux and OSX: @@ -285,7 +278,7 @@ schema and data to a file. It's a good idea to run this with the `--single-transaction` option to avoid locking your database tables while your backups run. It is also essential to use the `--routines` flag, which will include functions and stored procedures in the -backup (which ArchivesSpace uses at least for Jasper reports). +backup (which ArchivesSpace uses at least for reports). If you are running with the demo database, you can create periodic database snapshots using the following configuration settings: diff --git a/README_BACKGROUND_JOBS.md b/README_BACKGROUND_JOBS.md new file mode 100644 index 0000000000..13cd21e2fa --- /dev/null +++ b/README_BACKGROUND_JOBS.md @@ -0,0 +1,84 @@ +Background Jobs +============== + +ArchivesSpace provides a mechanism for long running processes to run asynchronously. These processes are called `Background Jobs`. + +## Managing Jobs in the Staff UI + +The `Create` menu has a `Background Job` option which shows a submenu of job types that the current user has permission to create. (See below for more information about job permissions and hidden jobs.) Selecting one of these options will take the user to a form to enter any parameters required for the job and then to create it. + +When a job is created it is placed in the `Background Job Queue`. Jobs in the queue will be run in the order they were created. (See below for more information about multiple threads and concurrent jobs.) + +The `Browse` menu has a `Background Jobs` option. This takes the user to a list of jobs arranged by their status. The user can then view the details of a job, and cancel it if they have permission. + + +## Permissions + +A user must have the `create_job` permission to create a job. By default, this permission is included in the `repository_basic_data_entry` group. + +A user must have the `cancel_job` permission to cancel a job. By default, this permission is included in the `repository_managers` group. + +When a JobRunner registers it can specify additional create and cancel permssions. (See below for more information) + + +## Types, Runners and Schemas + +Each job has a type, and each type has a registered runner to run jobs of that type and JSONModel schema to define its parameters. + +#### Registered JobRunners + +All jobs of a type are handled by a registered `JobRunner`. The job runner classes are located here: + + backend/app/lib/job_runners/ + +It is possible to define additional job runners from a plugin. (See below for more information about plugins.) + +A job runner class must subclass `JobRunner`, reigister to run one or more job types, and implement a `#run` method for jobs that it handles. + +When a job runner registers for a job type, it can set some options: + + * `:hidden` + * Defaults to `false` + * If this is set then this job type will not be shown in the list of available job types. + * `:run_concurrently` + * Defaults to `false` + * If this is set to true then more than one job of this type can run at the same time. + * `:create_permissions` + * Defaults to `[]` + * A permission or list of permissions required, in addition to `create_job`, to create jobs of this type. + * `:cancel_permissions` + * Defaults to `[]` + * A permission or list of permissions required, in addition to `cancel_job`, to cancel jobs of this type. + +For more information about defining a job runner, see the `JobRunner` superclass: + + backend/app/lib/job_runner.rb + +#### JSONModel Schemas + +A job type also requires a JSONModel schema that defines the parameters to run a job of the type. The schema name must be the same as the type that the runner registers for. For example: + + common/schemas/import_job.rb + +This schema, for `JSONModel(:import_job)`, defines the parameters for running a job of type `import_job`. + + +## Concurrency + +ArchivesSpace can be configured to run more than one background job at a time. By default, there will be two threads available to run background jobs. The configuration looks like this: + + AppConfig[:job_thread_count] = 2 + +The `BackgroundJobQueue` will start this number of threads at start up. Those threads will then poll for queued jobs and run them. + +When a job runner registers, it can set an option called `:run_concurrently`. This is `false` by default. When set to `false` a job thread will not run a job if there is already a job of that type running. The job will remain on the queue and will be run when there are no longer any jobs of its type running. + +When set to `true` a job will be run when it comes to the front of the queue regardless of whether there is a job of the same type running. + + +## Plugins + +It is possible to add a new job type from a plugin. ArchivesSpace includes a plugin that demonstrates how to do this: + + plugins/jobs_example + diff --git a/_yard/Gemfile.lock b/_yard/Gemfile.lock index ba3fdddbd5..381e506649 100644 --- a/_yard/Gemfile.lock +++ b/_yard/Gemfile.lock @@ -1,8 +1,8 @@ GEM remote: https://rubygems.org/ specs: - kramdown (1.11.1) - yard (0.9.0) + kramdown (1.13.2) + yard (0.9.8) PLATFORMS java diff --git a/backend/Gemfile b/backend/Gemfile index e6b897f9d0..c52ea62859 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -1,36 +1,36 @@ source "https://rubygems.org" gem "atomic", '= 1.0.1' -gem "sinatra", "1.3.6", :require => false -gem 'rack', '= 1.4.7' -gem "sinatra-reloader", :require => false +gem "sinatra", "1.4.7", :require => false +gem "sinatra-contrib", "1.4.7", :require => false gem "sequel", "~> 4.20.0" gem "rack-session-sequel", "0.0.1" gem "jdbc-mysql", "5.1.13", :group => :development -gem "jdbc-derby", "10.6.2.1" +gem "jdbc-derby", "10.12.1.1" gem "bcrypt", "3.1.7" -gem 'json', "1.8.0" +gem 'json', "1.8.6" gem "json-schema", "1.0.10" -gem "jruby-jars", "= 1.7.21" -gem "nokogiri", '~> 1.6.1' +gem "jruby-jars", "= 9.1.8.0" +gem "nokogiri", "1.7.0.1" gem "saxerator", "~> 0.9.2" gem 'saxon-xslt' -gem 'tzinfo', '~> 0.3.48' +gem 'tzinfo' gem "rufus-scheduler", "~> 2.0.24" gem "rufus-lru", "1.0.5" gem "net-ldap", "0.6.1" -gem "puma", "2.8.2" +gem "mizuno", "0.6.11" gem "i18n", ">= 0.6.4" gem "axlsx", "2.0.1" -gem "warbler", "1.4.9", :group => :build +gem "warbler", "2.0.4", :group => :build group :test do - gem "factory_girl", "~> 4.1.0" - gem "rspec", "~> 3.3.0" - gem "rspec-core", "~> 3.3.0" + gem "factory_girl" + gem "activesupport", "5.0.1" # Loaded by factory_girl + gem "rspec" + gem "rspec-core" gem "ladle", "0.2.0" gem "simplecov", "0.7.1" end @@ -43,6 +43,8 @@ gem "rjack-jackson", "1.8.11.0" gem "rubyzip", "1.0.0" gem "zip-zip", "0.3" +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] # Allow plugins to provide their own Gemfiles too. require 'asutils' diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index 974fd8f1da..fa393e89d7 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -1,12 +1,11 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.0.13) - i18n (~> 0.6, >= 0.6.9) - minitest (~> 4.2) - multi_json (~> 1.3) - thread_safe (~> 0.1) - tzinfo (~> 0.3.37) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) atomic (1.0.1-java) axlsx (2.0.1) htmlentities (~> 4.3.1) @@ -14,28 +13,35 @@ GEM rubyzip (~> 1.0.0) backports (3.6.8) bcrypt (3.1.7-java) - diff-lcs (1.2.5) - eventmachine (1.2.0.1-java) - factory_girl (4.1.0) + childprocess (0.6.2) + ffi (~> 1.0, >= 1.0.11) + choice (0.2.0) + concurrent-ruby (1.0.4-java) + diff-lcs (1.3) + factory_girl (4.8.0) activesupport (>= 3.0.0) + ffi (1.9.18-java) htmlentities (4.3.4) i18n (0.7.0) - jdbc-derby (10.6.2.1-java) + jdbc-derby (10.12.1.1) jdbc-mysql (5.1.13) - jruby-jars (1.7.21) + jruby-jars (9.1.8.0) jruby-rack (1.1.20) - json (1.8.0-java) + json (1.8.6-java) json-schema (1.0.10) ladle (0.2.0-java) - minitest (4.7.5) + minitest (5.10.1) + mizuno (0.6.11) + childprocess (>= 0.2.6) + choice (>= 0.1.0) + ffi (>= 1.0.0) + rack (>= 1.0.0) multi_json (1.12.1) multipart-post (1.2.0) net-http-persistent (2.8) net-ldap (0.6.1) - nokogiri (1.6.8-java) - puma (2.8.2-java) - rack (>= 1.1, < 2.0) - rack (1.4.7) + nokogiri (1.7.0.1-java) + rack (1.6.5) rack-protection (1.5.3) rack rack-session-sequel (0.0.1) @@ -43,21 +49,21 @@ GEM sequel rack-test (0.6.3) rack (>= 1.0) - rake (11.2.2) + rake (12.0.0) rjack-jackson (1.8.11.0-java) - rspec (3.3.0) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + rspec (3.5.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-support (3.3.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) rubyzip (1.0.0) rufus-lru (1.0.5) rufus-scheduler (2.0.24) @@ -70,27 +76,28 @@ GEM multi_json (~> 1.0) simplecov-html (~> 0.7.1) simplecov-html (0.7.1) - sinatra (1.3.6) - rack (~> 1.4) - rack-protection (~> 1.3) - tilt (~> 1.3, >= 1.3.3) - sinatra-contrib (1.3.2) + sinatra (1.4.7) + rack (~> 1.5) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + sinatra-contrib (1.4.7) backports (>= 2.0) - eventmachine + multi_json rack-protection rack-test - sinatra (~> 1.3.0) - tilt (~> 1.3) - sinatra-reloader (1.0) - sinatra-contrib + sinatra (~> 1.4.0) + tilt (>= 1.3, < 3) thread_safe (0.3.5-java) - tilt (1.4.1) - tzinfo (0.3.51) - warbler (1.4.9) - jruby-jars (>= 1.5.6, < 2.0) + tilt (2.0.6) + tzinfo (1.2.2) + thread_safe (~> 0.1) + tzinfo-data (1.2017.1) + tzinfo (>= 1.0.0) + warbler (2.0.4) + jruby-jars (>= 9.0.0.0) jruby-rack (>= 1.1.1, < 1.3) - rake (>= 0.9.6) - rubyzip (>= 0.9, < 1.2) + rake (>= 10.1.0) + rubyzip (~> 1.0, < 1.4) zip-zip (0.3) rubyzip (>= 1.0.0) @@ -98,27 +105,27 @@ PLATFORMS java DEPENDENCIES + activesupport (= 5.0.1) atomic (= 1.0.1) axlsx (= 2.0.1) bcrypt (= 3.1.7) - factory_girl (~> 4.1.0) + factory_girl i18n (>= 0.6.4) - jdbc-derby (= 10.6.2.1) + jdbc-derby (= 10.12.1.1) jdbc-mysql (= 5.1.13) - jruby-jars (= 1.7.21) - json (= 1.8.0) + jruby-jars (= 9.1.8.0) + json (= 1.8.6) json-schema (= 1.0.10) ladle (= 0.2.0) + mizuno (= 0.6.11) multipart-post (= 1.2.0) net-http-persistent (= 2.8) net-ldap (= 0.6.1) - nokogiri (~> 1.6.1) - puma (= 2.8.2) - rack (= 1.4.7) + nokogiri (= 1.7.0.1) rack-session-sequel (= 0.0.1) rjack-jackson (= 1.8.11.0) - rspec (~> 3.3.0) - rspec-core (~> 3.3.0) + rspec + rspec-core rubyzip (= 1.0.0) rufus-lru (= 1.0.5) rufus-scheduler (~> 2.0.24) @@ -126,10 +133,11 @@ DEPENDENCIES saxon-xslt sequel (~> 4.20.0) simplecov (= 0.7.1) - sinatra (= 1.3.6) - sinatra-reloader - tzinfo (~> 0.3.48) - warbler (= 1.4.9) + sinatra (= 1.4.7) + sinatra-contrib (= 1.4.7) + tzinfo + tzinfo-data + warbler (= 2.0.4) zip-zip (= 0.3) BUNDLED WITH diff --git a/backend/app/controllers/agent_corporate_entity.rb b/backend/app/controllers/agent_corporate_entity.rb index 3139e25b02..dfd6122fd5 100644 --- a/backend/app/controllers/agent_corporate_entity.rb +++ b/backend/app/controllers/agent_corporate_entity.rb @@ -46,7 +46,8 @@ class ArchivesSpaceService < Sinatra::Base .returns([200, "(:agent_corporate_entity)"], [404, "Not found"]) \ do - json_response(resolve_references(AgentCorporateEntity.to_jsonmodel(AgentCorporateEntity.get_or_die(params[:id])), + opts = {:calculate_linked_repositories => current_user.can?(:index_system)} + json_response(resolve_references(AgentCorporateEntity.to_jsonmodel(AgentCorporateEntity.get_or_die(params[:id]), opts), params[:resolve])) end diff --git a/backend/app/controllers/agent_family.rb b/backend/app/controllers/agent_family.rb index d4ec4b8fae..a29defc130 100644 --- a/backend/app/controllers/agent_family.rb +++ b/backend/app/controllers/agent_family.rb @@ -46,7 +46,8 @@ class ArchivesSpaceService < Sinatra::Base .returns([200, "(:agent)"], [404, "Not found"]) \ do - json_response(resolve_references(AgentFamily.to_jsonmodel(AgentFamily.get_or_die(params[:id])), + opts = {:calculate_linked_repositories => current_user.can?(:index_system)} + json_response(resolve_references(AgentFamily.to_jsonmodel(AgentFamily.get_or_die(params[:id]), opts), params[:resolve])) end diff --git a/backend/app/controllers/agent_person.rb b/backend/app/controllers/agent_person.rb index 0e47d72cc6..8b9ff20530 100644 --- a/backend/app/controllers/agent_person.rb +++ b/backend/app/controllers/agent_person.rb @@ -46,7 +46,8 @@ class ArchivesSpaceService < Sinatra::Base .returns([200, "(:agent)"], [404, "Not found"]) \ do - json_response(resolve_references(AgentPerson.to_jsonmodel(AgentPerson.get_or_die(params[:id])), + opts = {:calculate_linked_repositories => current_user.can?(:index_system)} + json_response(resolve_references(AgentPerson.to_jsonmodel(AgentPerson.get_or_die(params[:id]), opts), params[:resolve])) end diff --git a/backend/app/controllers/agent_software.rb b/backend/app/controllers/agent_software.rb index e0f2e7bad9..797d68af2f 100644 --- a/backend/app/controllers/agent_software.rb +++ b/backend/app/controllers/agent_software.rb @@ -46,7 +46,8 @@ class ArchivesSpaceService < Sinatra::Base .returns([200, "(:agent)"], [404, "Not found"]) \ do - json_response(resolve_references(AgentSoftware.to_jsonmodel(AgentSoftware.get_or_die(params[:id])), + opts = {:calculate_linked_repositories => current_user.can?(:index_system)} + json_response(resolve_references(AgentSoftware.to_jsonmodel(AgentSoftware.get_or_die(params[:id]), opts), params[:resolve])) end diff --git a/backend/app/controllers/archival_object.rb b/backend/app/controllers/archival_object.rb index ecf48c552d..f7a2c8ba14 100644 --- a/backend/app/controllers/archival_object.rb +++ b/backend/app/controllers/archival_object.rb @@ -36,7 +36,7 @@ class ArchivesSpaceService < Sinatra::Base [400, :error]) \ do obj = ArchivalObject.get_or_die(params[:id]) - obj.update_position_only(params[:parent], params[:position]) + obj.set_parent_and_position(params[:parent], params[:position]) updated_response(obj) end diff --git a/backend/app/controllers/batch_import.rb b/backend/app/controllers/batch_import.rb index 5e1b9d8d37..b047dc924c 100644 --- a/backend/app/controllers/batch_import.rb +++ b/backend/app/controllers/batch_import.rb @@ -51,7 +51,8 @@ class ArchivesSpaceService < Sinatra::Base # Wrap the import in a transaction if the DB supports MVCC begin DB.open(DB.supports_mvcc?, - :retry_on_optimistic_locking_fail => true) do + :retry_on_optimistic_locking_fail => true, + :isolation_level => :committed) do last_error = nil File.open(env['batch_import_file']) do |stream| diff --git a/backend/app/controllers/classification.rb b/backend/app/controllers/classification.rb index 99bee927a6..98beb1cd1e 100644 --- a/backend/app/controllers/classification.rb +++ b/backend/app/controllers/classification.rb @@ -1,3 +1,5 @@ +require_relative 'tree_docs' + class ArchivesSpaceService < Sinatra::Base Endpoint.post('/repositories/:repo_id/classifications') @@ -28,6 +30,8 @@ class ArchivesSpaceService < Sinatra::Base Endpoint.get('/repositories/:repo_id/classifications/:id/tree') .description("Get a Classification tree") + .deprecated("Call the */tree/{root,waypoint,node} endpoints to traverse record trees." + + " See backend/app/model/large_tree.rb for further information.") .params(["id", :id], ["repo_id", :repo_id]) .permissions([:view_repository]) @@ -72,4 +76,78 @@ class ArchivesSpaceService < Sinatra::Base do handle_delete(Classification, params[:id]) end + + ## Trees! + + Endpoint.get('/repositories/:repo_id/classifications/:id/tree/root') + .description("Fetch tree information for the top-level classification record") + .params(["id", :id], + ["repo_id", :repo_id], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::ROOT_DOCS]) \ + do + json_response(large_tree_for_classification.root) + end + + Endpoint.get('/repositories/:repo_id/classifications/:id/tree/waypoint') + .description("Fetch the record slice for a given tree waypoint") + .params(["id", :id], + ["repo_id", :repo_id], + ["offset", Integer, "The page of records to return"], + ["parent_node", String, "The URI of the parent of this waypoint (none for the root record)", :optional => true], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::WAYPOINT_DOCS]) \ + do + offset = params[:offset] + + parent_id = if params[:parent_node] + JSONModel.parse_reference(params[:parent_node]).fetch(:id) + else + # top-level record + nil + end + + json_response(large_tree_for_classification.waypoint(parent_id, offset)) + end + + Endpoint.get('/repositories/:repo_id/classifications/:id/tree/node') + .description("Fetch tree information for an Classification Term record within a tree") + .params(["id", :id], + ["repo_id", :repo_id], + ["node_uri", String, "The URI of the Classification Term record of interest"], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::NODE_DOCS]) \ + do + classification_term_id = JSONModel.parse_reference(params[:node_uri]).fetch(:id) + + json_response(large_tree_for_classification.node(ClassificationTerm.get_or_die(classification_term_id))) + end + + Endpoint.get('/repositories/:repo_id/classifications/:id/tree/node_from_root') + .description("Fetch tree path from the root record to Classification Terms") + .params(["id", :id], + ["repo_id", :repo_id], + ["node_ids", [Integer], "The IDs of the Classification Term records of interest"], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::NODE_FROM_ROOT_DOCS]) \ + do + json_response(large_tree_for_classification.node_from_root(params[:node_ids], params[:repo_id])) + end + + private + + def large_tree_for_classification(largetree_opts = {}) + classification = Classification.get_or_die(params[:id]) + + large_tree = LargeTree.new(classification, {:published_only => params[:published_only]}.merge(largetree_opts)) + large_tree.add_decorator(LargeTreeClassification.new) + + large_tree + end + + end diff --git a/backend/app/controllers/component_add_children.rb b/backend/app/controllers/component_add_children.rb index 6ce677337d..335b1cb872 100644 --- a/backend/app/controllers/component_add_children.rb +++ b/backend/app/controllers/component_add_children.rb @@ -175,29 +175,22 @@ def accept_children_response(target_class, child_class) # Does this cause any undo problems? first_uri = params[:children][0] first_obj = child_class.get_or_die(child_class.my_jsonmodel.id_for(first_uri)) - - + # ok, we are keeping it in the same parent and moving down the list, we # need to reverse to make sure the placement happens correctly. # If the first_obj doesn't have a parent_id, that means it's at the top # of the food chain, so we can check if the target is a Tree, not a TreeNode. # Otherwise, we are moving into another parent. - if ( target.id == first_obj.parent_id || ( target.class.included_modules.include?(Trees) && first_obj.parent_id.nil? ) ) && first_obj.absolute_position < position + if ( target.id == first_obj.parent_id || ( target.class.included_modules.include?(Trees) && first_obj.parent_id.nil? ) ) && first_obj.logical_position < position ordered = params[:children].each_with_index.to_a.reverse else ordered = params[:children].each_with_index end - - begin - last_child = nil - ordered.each do |uri, i| - last_child = child_class.get_or_die(child_class.my_jsonmodel.id_for(uri)) - last_child.update_position_only(parent_id, position + i ) - end - ensure - # close out the gaps. - last_child.order_siblings if last_child + last_child = nil + ordered.each do |uri, i| + last_child = child_class.get_or_die(child_class.my_jsonmodel.id_for(uri)) + last_child.set_parent_and_position(parent_id, position + i) end end diff --git a/backend/app/controllers/digital_object.rb b/backend/app/controllers/digital_object.rb index f873ade4ca..70d294fd08 100644 --- a/backend/app/controllers/digital_object.rb +++ b/backend/app/controllers/digital_object.rb @@ -49,9 +49,10 @@ class ArchivesSpaceService < Sinatra::Base handle_listing(DigitalObject, params) end - Endpoint.get('/repositories/:repo_id/digital_objects/:id/tree') .description("Get a Digital Object tree") + .deprecated("Call the */tree/{root,waypoint,node} endpoints to traverse record trees." + + " See backend/app/model/large_tree.rb for further information.") .params(["id", :id], ["repo_id", :repo_id]) .permissions([:view_repository]) @@ -88,4 +89,76 @@ class ArchivesSpaceService < Sinatra::Base updated_response(digital_object) end + ## Trees! + + Endpoint.get('/repositories/:repo_id/digital_objects/:id/tree/root') + .description("Fetch tree information for the top-level digital object record") + .params(["id", :id], + ["repo_id", :repo_id], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::ROOT_DOCS]) \ + do + json_response(large_tree_for_digital_object.root) + end + + Endpoint.get('/repositories/:repo_id/digital_objects/:id/tree/waypoint') + .description("Fetch the record slice for a given tree waypoint") + .params(["id", :id], + ["repo_id", :repo_id], + ["offset", Integer, "The page of records to return"], + ["parent_node", String, "The URI of the parent of this waypoint (none for the root record)", :optional => true], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::WAYPOINT_DOCS]) \ + do + offset = params[:offset] + + parent_id = if params[:parent_node] + JSONModel.parse_reference(params[:parent_node]).fetch(:id) + else + # top-level record + nil + end + + json_response(large_tree_for_digital_object.waypoint(parent_id, offset)) + end + + Endpoint.get('/repositories/:repo_id/digital_objects/:id/tree/node') + .description("Fetch tree information for an Digital Object Component record within a tree") + .params(["id", :id], + ["repo_id", :repo_id], + ["node_uri", String, "The URI of the Digital Object Component record of interest"], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::NODE_DOCS]) \ + do + digital_object_component_id = JSONModel.parse_reference(params[:node_uri]).fetch(:id) + + json_response(large_tree_for_digital_object.node(DigitalObjectComponent.get_or_die(digital_object_component_id))) + end + + Endpoint.get('/repositories/:repo_id/digital_objects/:id/tree/node_from_root') + .description("Fetch tree paths from the root record to Digital Object Components") + .params(["id", :id], + ["repo_id", :repo_id], + ["node_ids", [Integer], "The IDs of the Digital Object Component records of interest"], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::NODE_FROM_ROOT_DOCS]) \ + do + json_response(large_tree_for_digital_object.node_from_root(params[:node_ids], params[:repo_id])) + end + + private + + def large_tree_for_digital_object(largetree_opts = {}) + digital_object = DigitalObject.get_or_die(params[:id]) + + large_tree = LargeTree.new(digital_object, {:published_only => params[:published_only]}.merge(largetree_opts)) + large_tree.add_decorator(LargeTreeDigitalObject.new) + + large_tree + end + end diff --git a/backend/app/controllers/digital_object_component.rb b/backend/app/controllers/digital_object_component.rb index 4e112e4b0d..db5dc4bef2 100644 --- a/backend/app/controllers/digital_object_component.rb +++ b/backend/app/controllers/digital_object_component.rb @@ -36,7 +36,7 @@ class ArchivesSpaceService < Sinatra::Base [400, :error]) \ do obj = DigitalObjectComponent.get_or_die(params[:id]) - obj.update_position_only(params[:parent], params[:position]) + obj.set_parent_and_position(params[:parent], params[:position]) updated_response(obj) end diff --git a/backend/app/controllers/exports.rb b/backend/app/controllers/exports.rb index baa13176ae..f207fbf60e 100644 --- a/backend/app/controllers/exports.rb +++ b/backend/app/controllers/exports.rb @@ -48,7 +48,7 @@ class ArchivesSpaceService < Sinatra::Base .returns([200, "The export metadata"]) \ do json_response({"filename" => - safe_filenmae(DigitalObject[params[:id]].digital_object_id, "_mets.xml"), + safe_filename(DigitalObject[params[:id]].digital_object_id, "_mets.xml"), "mimetype" => "application/xml"}) end diff --git a/backend/app/controllers/job.rb b/backend/app/controllers/job.rb index 2cb0228cd3..8bcc74ec6b 100644 --- a/backend/app/controllers/job.rb +++ b/backend/app/controllers/job.rb @@ -1,13 +1,38 @@ class ArchivesSpaceService < Sinatra::Base + # Job runners can specify permissions required to create or cancel + # particular types of jobs, so we have special handling for it here + + def has_permissions_or_raise(job, permissions) + runner = JobRunner.registered_runner_for(job['job']['jsonmodel_type']) + + runner.send(permissions).each do |perm| + unless current_user.can?(perm) + raise AccessDeniedException.new("Access denied") + end + end + end + + + def can_create_or_raise(job) + has_permissions_or_raise(job, :create_permissions) + end + + + def can_cancel_or_raise(job) + has_permissions_or_raise(job, :cancel_permissions) + end + Endpoint.post('/repositories/:repo_id/jobs') - .description("Create a new import job") + .description("Create a new job") .params(["job", JSONModel(:job), "The job object", :body => true], ["repo_id", :repo_id]) - .permissions([:import_records]) + .permissions([:create_job]) .returns([200, :updated]) \ do + can_create_or_raise(params[:job]) + job = Job.create_from_json(params[:job], :user => current_user) created_response(job, params[:job]) @@ -15,13 +40,15 @@ class ArchivesSpaceService < Sinatra::Base Endpoint.post('/repositories/:repo_id/jobs_with_files') - .description("Create a new import job and post input files") + .description("Create a new job and post input files") .params(["job", JSONModel(:job)], ["files", [UploadFile]], ["repo_id", :repo_id]) - .permissions([:import_records]) + .permissions([:create_job]) .returns([200, :updated]) \ do + can_create_or_raise(params[:job]) + job = Job.create_from_json(params[:job], :user => current_user) params[:files].each do |file| @@ -32,16 +59,13 @@ class ArchivesSpaceService < Sinatra::Base end - Endpoint.get('/repositories/:repo_id/jobs/types') - .description("List all supported import job types") - .params(["repo_id", :repo_id]) + Endpoint.get('/job_types') + .description("List all supported job types") + .params() .permissions([]) .returns([200, "A list of supported job types"]) \ do - show_hidden = false - # json_response(Converter.list_import_types(show_hidden)) - e = Enumeration.filter(:name => 'job_type').first - json_response(Enumeration.to_jsonmodel(e).values) + json_response(JobRunner.registered_job_types) end @@ -59,12 +83,14 @@ class ArchivesSpaceService < Sinatra::Base Endpoint.post('/repositories/:repo_id/jobs/:id/cancel') - .description("Cancel a job") + .description("Cancel a Job") .params(["id", :id], ["repo_id", :repo_id]) - .permissions([:cancel_importer_job]) + .permissions([:cancel_job]) .returns([200, :updated]) \ do + can_cancel_or_raise(Job.to_jsonmodel(params[:id])) + job = Job.get_or_die(params[:id]) job.cancel! diff --git a/backend/app/controllers/repository.rb b/backend/app/controllers/repository.rb index e92499b1c8..37ce5ab1a1 100644 --- a/backend/app/controllers/repository.rb +++ b/backend/app/controllers/repository.rb @@ -142,17 +142,20 @@ class ArchivesSpaceService < Sinatra::Base Endpoint.get('/repositories/:id') .description("Get a Repository by ID") - .params(["id", :id]) + .params(["id", :id], + ["resolve", :resolve]) .permissions([]) .returns([200, "(:repository)"], [404, "Not found"]) \ do - json_response(Repository.to_jsonmodel(Repository.get_or_die(params[:id]))) + json_response(resolve_references(Repository.to_jsonmodel(Repository.get_or_die(params[:id])), + params[:resolve])) end Endpoint.get('/repositories') .description("Get a list of Repositories") + .params(["resolve", :resolve]) .permissions([]) .returns([200, "[(:repository)]"]) \ do diff --git a/backend/app/controllers/resource.rb b/backend/app/controllers/resource.rb index 7ce05e0a42..33e2af0eb9 100644 --- a/backend/app/controllers/resource.rb +++ b/backend/app/controllers/resource.rb @@ -1,3 +1,5 @@ +require_relative 'tree_docs' + class ArchivesSpaceService < Sinatra::Base Endpoint.post('/repositories/:repo_id/resources') @@ -28,6 +30,8 @@ class ArchivesSpaceService < Sinatra::Base Endpoint.get('/repositories/:repo_id/resources/:id/tree') .description("Get a Resource tree") + .deprecated("Call the */tree/{root,waypoint,node} endpoints to traverse record trees." + + " See backend/app/model/large_tree.rb for further information.") .params(["id", :id], ["limit_to", String, "An Archival Object URI or 'root'", :optional => true], ["repo_id", :repo_id]) @@ -58,6 +62,20 @@ class ArchivesSpaceService < Sinatra::Base end + Endpoint.get('/repositories/:repo_id/resources/:id/ordered_records') + .description("Get the list of URIs of this resource and all archival objects contained within." + + "Ordered by tree order (i.e. if you fully expanded the record tree and read from top to bottom)") + .params(["id", :id], + ["repo_id", :repo_id]) + .permissions([:view_repository]) + .returns([200, "JSONModel(:resource_ordered_records)"]) \ + do + resource = Resource.get_or_die(params[:id]) + + json_response(JSONModel(:resource_ordered_records).from_hash(:uris => resource.ordered_records)) + end + + Endpoint.post('/repositories/:repo_id/resources/:id') .description("Update a Resource") .params(["id", :id], @@ -124,4 +142,76 @@ class ArchivesSpaceService < Sinatra::Base json_response(record_types) end + ## Trees! + + Endpoint.get('/repositories/:repo_id/resources/:id/tree/root') + .description("Fetch tree information for the top-level resource record") + .params(["id", :id], + ["repo_id", :repo_id], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::ROOT_DOCS]) \ + do + json_response(large_tree_for_resource.root) + end + + Endpoint.get('/repositories/:repo_id/resources/:id/tree/waypoint') + .description("Fetch the record slice for a given tree waypoint") + .params(["id", :id], + ["repo_id", :repo_id], + ["offset", Integer, "The page of records to return"], + ["parent_node", String, "The URI of the parent of this waypoint (none for the root record)", :optional => true], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::WAYPOINT_DOCS]) \ + do + offset = params[:offset] + + parent_id = if params[:parent_node] + JSONModel.parse_reference(params[:parent_node]).fetch(:id) + else + # top-level record + nil + end + + json_response(large_tree_for_resource.waypoint(parent_id, offset)) + end + + Endpoint.get('/repositories/:repo_id/resources/:id/tree/node') + .description("Fetch tree information for an Archival Object record within a tree") + .params(["id", :id], + ["repo_id", :repo_id], + ["node_uri", String, "The URI of the Archival Object record of interest"], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::NODE_DOCS]) \ + do + ao_id = JSONModel.parse_reference(params[:node_uri]).fetch(:id) + + json_response(large_tree_for_resource.node(ArchivalObject.get_or_die(ao_id))) + end + + Endpoint.get('/repositories/:repo_id/resources/:id/tree/node_from_root') + .description("Fetch tree paths from the root record to Archival Objects") + .params(["id", :id], + ["repo_id", :repo_id], + ["node_ids", [Integer], "The IDs of the Archival Object records of interest"], + ["published_only", BooleanParam, "Whether to restrict to published/unsuppressed items", :default => false]) + .permissions([:view_repository]) + .returns([200, TreeDocs::NODE_FROM_ROOT_DOCS]) \ + do + json_response(large_tree_for_resource.node_from_root(params[:node_ids], params[:repo_id])) + end + + private + + def large_tree_for_resource(largetree_opts = {}) + resource = Resource.get_or_die(params[:id]) + + large_tree = LargeTree.new(resource, {:published_only => params[:published_only]}.merge(largetree_opts)) + large_tree.add_decorator(LargeTreeResource.new) + + large_tree + end + end diff --git a/backend/app/controllers/search.rb b/backend/app/controllers/search.rb index 7678f45661..1e3384b560 100644 --- a/backend/app/controllers/search.rb +++ b/backend/app/controllers/search.rb @@ -1,3 +1,5 @@ +require 'advanced_query_builder' + class ArchivesSpaceService < Sinatra::Base BASE_SEARCH_PARAMS = @@ -17,11 +19,13 @@ class ArchivesSpaceService < Sinatra::Base [String], "The list of the fields to produce facets for", :optional => true], - ["filter_term", [String], "A json string containing the term/value pairs to be applied as filters. Of the form: {\"fieldname\": \"fieldvalue\"}.", + ["facet_mincount", + Integer, + "The minimum count for a facet field to be included in the response", :optional => true], - ["simple_filter", [String], "A simple direct filter to be applied as a filter. Of the form 'primary_type:accession OR primary_type:agent_person'.", + ["filter", JSONModel(:advanced_query), "A json string containing the advanced query to filter by", :optional => true], - ["exclude", + ["exclude", [String], "A list of document IDs that should be excluded from results", :optional => true], @@ -40,7 +44,7 @@ class ArchivesSpaceService < Sinatra::Base ] - Endpoint.get('/repositories/:repo_id/search') + Endpoint.get_or_post('/repositories/:repo_id/search') .description("Search this repository") .params(["repo_id", :repo_id], *BASE_SEARCH_PARAMS) @@ -56,7 +60,7 @@ class ArchivesSpaceService < Sinatra::Base end - Endpoint.get('/search') + Endpoint.get_or_post('/search') .description("Search this archive") .params(*BASE_SEARCH_PARAMS) .permissions([:view_all_records]) @@ -67,7 +71,7 @@ class ArchivesSpaceService < Sinatra::Base end - Endpoint.get('/search/repositories') + Endpoint.get_or_post('/search/repositories') .description("Search across repositories") .params(*BASE_SEARCH_PARAMS) .permissions([]) @@ -78,7 +82,41 @@ class ArchivesSpaceService < Sinatra::Base end - Endpoint.get('/search/subjects') + Endpoint.get_or_post('/search/records') + .description("Return a set of records by URI") + .params(["uri", + [String], + "The list of record URIs to fetch"], + ["resolve", + [String], + "The list of result fields to resolve (if any)", + :optional => true]) + .permissions([:view_all_records]) + .returns([200, "a JSON map of records"]) \ + do + records = Search.records_for_uris(Array(params[:uri]), Array(params[:resolve])) + + json_response(records) + end + + Endpoint.get_or_post('/search/record_types_by_repository') + .description("Return the counts of record types of interest by repository") + .params(["record_types", [String], "The list of record types to tally"], + ["repo_uri", + String, + "An optional repository URI. If given, just return counts for the single repository", + :optional => true]) + .permissions([:view_all_records]) + .returns([200, + "If repository is given, returns a map like " + + "{'record_type' => }." + + " Otherwise, {'repo_uri' => {'record_type' => }}"]) \ + do + json_response(Search.record_type_counts(params[:record_types], params[:repo_uri])) + end + + + Endpoint.get_or_post('/search/subjects') .description("Search across subjects") .params(*BASE_SEARCH_PARAMS) .permissions([]) @@ -109,11 +147,7 @@ class ArchivesSpaceService < Sinatra::Base set_record_types(['tree_view']). show_suppressed(show_suppressed). show_excluded_docs(true). - set_filter_terms([ - { - :node_uri => params[:node_uri] - }.to_json - ]) + set_filter(AdvancedQueryBuilder.new.and('node_uri', params[:node_uri]).build) search_data = Solr.search(query) diff --git a/backend/app/controllers/subject.rb b/backend/app/controllers/subject.rb index 05b241f6d3..afc967459c 100644 --- a/backend/app/controllers/subject.rb +++ b/backend/app/controllers/subject.rb @@ -43,7 +43,8 @@ class ArchivesSpaceService < Sinatra::Base .permissions([]) .returns([200, "(:subject)"]) \ do - json_response(Subject.to_jsonmodel(params[:id])) + opts = {:calculate_linked_repositories => current_user.can?(:index_system)} + json_response(Subject.to_jsonmodel(params[:id], opts)) end diff --git a/backend/app/controllers/system.rb b/backend/app/controllers/system.rb index 04e3f117ff..2446182116 100644 --- a/backend/app/controllers/system.rb +++ b/backend/app/controllers/system.rb @@ -29,27 +29,13 @@ class ArchivesSpaceService < Sinatra::Base [200, {}, SystemEvent.all.collect { |a| a.values }.to_json ] end - Endpoint.get('/system/resequence') - .description("Get the log information and start the 15 second log recorder") - .permissions([:administer_system]) - .params( ["types", [String], "Array of Object trypes to resequence", :optional => true] ) - .returns([200, "String"], - [403, "Access Denied"]) \ - do - klasses = %W{ ArchivalObject DigitalObjectComponent ClassificationTerm } & params[:type] - klasses.collect! { |k| Kernel.const_get(k.to_sym) } - Resequencer.run(klasses) - - [200, {}, Resequencer.status.to_s ] - end - Endpoint.post('/system/demo_db_snapshot') .description("Create a snapshot of the demo database if the file '#{File.basename(AppConfig[:demodb_snapshot_flag])}' exists in the data directory") .permissions([]) .returns([200, "OK"]) \ do flag = AppConfig[:demodb_snapshot_flag] - if File.exists?(flag) + if File.exist?(flag) Log.info("Starting backup of embedded demo database") DB.demo_db_backup Log.info("Backup of embedded demo database completed!") diff --git a/backend/app/controllers/tree_docs.rb b/backend/app/controllers/tree_docs.rb new file mode 100644 index 0000000000..7de512b736 --- /dev/null +++ b/backend/app/controllers/tree_docs.rb @@ -0,0 +1,62 @@ +# Putting some documentation in constants here because the same descriptions +# apply to all tree types. + +class TreeDocs + + ROOT_DOCS = < event_template('acknowledgement_sent'), - :agreement_received_event_date => event_template('agreement_received'), - :agreement_sent_event_date => event_template('agreement_sent'), - :cataloged_event_date => event_template('cataloged'), - :processed_event_date => event_template('processed'), - :agent => { :record_type => Proc.new {|data| @agent_type = data['agent_type'] diff --git a/backend/app/converters/converter.rb b/backend/app/converters/converter.rb index 9b9f3226cc..d9813843fc 100644 --- a/backend/app/converters/converter.rb +++ b/backend/app/converters/converter.rb @@ -1,6 +1,53 @@ require_relative 'lib/parse_queue' + +# +# `Converter` is an interface used to implement new importer types. To +# implement your own converter, create a subclass of this class and implement +# the "IMPLEMENT ME" methods marked below. +# class Converter + # Implement this in your Converter class! + # + # Returns descriptive metadata for the import type(s) implemented by this + # Converter. + def self.import_types(show_hidden = false) + raise NotImplementedError.new + + # Example: + [ + { + :name => "my_import_type", + :description => "Description of new importer" + } + ] + end + + # Implement this in your Converter class! + # + # If this Converter will handle `type` and `input_file`, return an instance. + def self.instance_for(type, input_file) + raise NotImplementedError.new + + # Example: + if type == "my_import_type" + self.new(input_file) + else + nil + end + end + + # Implement this in your Converter class! + # + # Process @input_file and load records into @batch. + def run + raise NotImplementedError.new + end + + + ## + ## That's it! Other implementation bits follow... + class ConverterMappingError < StandardError; end class ConverterNotFound < StandardError; end diff --git a/backend/app/converters/digital_object_converter.rb b/backend/app/converters/digital_object_converter.rb index ba999991ad..26199c45ae 100644 --- a/backend/app/converters/digital_object_converter.rb +++ b/backend/app/converters/digital_object_converter.rb @@ -25,11 +25,6 @@ def self.instance_for(type, input_file) end - def self.profile - "Convert Digital Object Records from a CSV file" - end - - def self.configure { # 1. Map the cell data to schemas or handlers diff --git a/backend/app/converters/eac_converter.rb b/backend/app/converters/eac_converter.rb index 27c5bd17bb..f7c2c1075b 100644 --- a/backend/app/converters/eac_converter.rb +++ b/backend/app/converters/eac_converter.rb @@ -25,11 +25,6 @@ def self.import_types(show_hidden = false) ] end - - def self.profile - "Convert EAC-CPF To ArchivesSpace JSONModel records" - end - end EACConverter.configure do |config| diff --git a/backend/app/converters/ead_converter.rb b/backend/app/converters/ead_converter.rb index 6bfd349ea9..13f5c66542 100644 --- a/backend/app/converters/ead_converter.rb +++ b/backend/app/converters/ead_converter.rb @@ -25,10 +25,6 @@ def self.instance_for(type, input_file) end - def self.profile - "Convert EAD To ArchivesSpace JSONModel records" - end - # We override this to skip nodes that are often very deep # We can safely assume ead, c, and archdesc will have children, # which greatly helps the performance. @@ -102,11 +98,11 @@ def self.configure ignore "titlepage" # addresses https://archivesspace.atlassian.net/browse/AR-1282 - with 'eadheader' do + with 'eadheader' do |*| set :finding_aid_status, att('findaidstatus') end - with 'archdesc' do + with 'archdesc' do |*| set :level, att('level') || 'otherlevel' set :other_level, att('otherlevel') set :publish, att('audience') != 'internal' @@ -116,7 +112,7 @@ def self.configure # c, c1, c2, etc... (0..12).to_a.map {|i| "c" + (i+100).to_s[1..-1]}.push('c').each do |c| - with c do + with c do |*| make :archival_object, { :level => att('level') || 'otherlevel', :other_level => att('otherlevel'), @@ -178,8 +174,7 @@ def self.configure end end - - with "archdesc/note" do + with "archdesc/note" do |*| make :note_multipart, { :type => 'odd', :persistent_id => att('id'), @@ -194,8 +189,7 @@ def self.configure end end - - with "langmaterial" do + with "langmaterial" do |*| # first, assign the primary language to the ead langmaterial = Nokogiri::XML::DocumentFragment.parse(inner_xml) langmaterial.children.each do |child| @@ -255,11 +249,10 @@ def make_nested_note(note_name, tag) end end - with 'physdesc' do + with 'physdesc' do |*| physdesc = Nokogiri::XML::DocumentFragment.parse(inner_xml) extent_number_and_type = nil - dimensions = [] physfacets = [] container_summaries = [] @@ -342,7 +335,7 @@ def make_nested_note(note_name, tag) end - with 'bibliography' do + with 'bibliography' do |*| make :note_bibliography set :persistent_id, att('id') set :publish, att('audience') != 'internal' @@ -350,7 +343,7 @@ def make_nested_note(note_name, tag) end - with 'index' do + with 'index' do |*| make :note_index set :persistent_id, att('id') set :publish, att('audience') != 'internal' @@ -363,13 +356,13 @@ def make_nested_note(note_name, tag) set :label, format_content( inner_xml ) end - with "#{x}/p" do + with "#{x}/p" do |*| set :content, format_content( inner_xml ) end end - with 'bibliography/bibref' do + with 'bibliography/bibref' do |*| set :items, inner_xml end @@ -397,7 +390,7 @@ def make_nested_note(note_name, tag) end # this is very imperfect. - with 'indexentry/ref' do + with 'indexentry/ref' do |*| make :note_index_item, { :type => 'name', :value => inner_xml, @@ -454,12 +447,12 @@ def make_nested_note(note_name, tag) end - with 'notestmt/note' do + with 'notestmt/note' do |*| append :finding_aid_note, format_content( inner_xml ) end - with 'chronlist' do + with 'chronlist' do |*| if ancestor(:note_multipart) left_overs = insert_into_subnotes else @@ -486,20 +479,20 @@ def make_nested_note(note_name, tag) end - with 'chronitem' do + with 'chronitem' do |*| context_obj.items << {} end %w(eventgrp/event chronitem/event).each do |path| - with path do + with path do |*| context_obj.items.last['events'] ||= [] context_obj.items.last['events'] << format_content( inner_xml ) end end - with 'list' do + with 'list' do |*| if ancestor(:note_multipart) left_overs = insert_into_subnotes @@ -560,17 +553,17 @@ def make_nested_note(note_name, tag) end - with 'list/item' do + with 'list/item' do |*| set :items, inner_xml if context == :note_orderedlist end - with 'publicationstmt/date' do + with 'publicationstmt/date' do |*| set :finding_aid_date, inner_xml if context == :resource end - with 'date' do + with 'date' do |*| if context == :note_chronology date = inner_xml context_obj.items.last['event_date'] = date @@ -578,7 +571,7 @@ def make_nested_note(note_name, tag) end - with 'head' do + with 'head' do |*| if context == :note_multipart set :label, format_content( inner_xml ) elsif context == :note_chronology @@ -588,7 +581,7 @@ def make_nested_note(note_name, tag) # example of a 1:many tag:record relation (1+ => 1 instance with 1 container) - with 'container' do + with 'container' do |*| @containers ||= {} # we've found that the container has a parent att and the parent is in @@ -649,38 +642,38 @@ def make_nested_note(note_name, tag) end - with 'author' do + with 'author' do |*| set :finding_aid_author, inner_xml end - with 'descrules' do + with 'descrules' do |*| set :finding_aid_description_rules, format_content( inner_xml ) end - with 'eadid' do + with 'eadid' do |*| set :ead_id, inner_xml set :ead_location, att('url') end - with 'editionstmt' do + with 'editionstmt' do |*| set :finding_aid_edition_statement, format_content( inner_xml ) end - with 'seriesstmt' do + with 'seriesstmt' do |*| set :finding_aid_series_statement, format_content( inner_xml ) end - with 'sponsor' do + with 'sponsor' do |*| set :finding_aid_sponsor, format_content( inner_xml ) end - with 'titleproper' do + with 'titleproper' do |*| type = att('type') case type when 'filing' @@ -690,54 +683,54 @@ def make_nested_note(note_name, tag) end end - with 'subtitle' do + with 'subtitle' do |*| set :finding_aid_subtitle, format_content( inner_xml ) end - with 'langusage' do + with 'langusage' do |*| set :finding_aid_language, format_content( inner_xml ) end - with 'revisiondesc/change' do + with 'revisiondesc/change' do |*| make :revision_statement set ancestor(:resource), :revision_statements, proxy end - with 'revisiondesc/change/item' do + with 'revisiondesc/change/item' do |*| set :description, format_content( inner_xml ) end - with 'revisiondesc/change/date' do + with 'revisiondesc/change/date' do |*| set :date, format_content( inner_xml ) end - with 'origination/corpname' do + with 'origination/corpname' do |*| make_corp_template(:role => 'creator') end - with 'controlaccess/corpname' do + with 'controlaccess/corpname' do |*| make_corp_template(:role => 'subject') end - with 'origination/famname' do + with 'origination/famname' do |*| make_family_template(:role => 'creator') end - with 'controlaccess/famname' do + with 'controlaccess/famname' do |*| make_family_template(:role => 'subject') end - with 'origination/persname' do + with 'origination/persname' do |*| make_person_template(:role => 'creator') end - with 'controlaccess/persname' do + with 'controlaccess/persname' do |*| make_person_template(:role => 'subject') end @@ -749,7 +742,7 @@ def make_nested_note(note_name, tag) 'occupation' => 'occupation', 'subject' => 'topical' }.each do |tag, type| - with "controlaccess/#{tag}" do + with "controlaccess/#{tag}" do |*| make :subject, { :terms => {'term' => inner_xml, 'term_type' => type, 'vocabulary' => '/vocabularies/1'}, :vocabulary => '/vocabularies/1', @@ -761,7 +754,7 @@ def make_nested_note(note_name, tag) end - with 'dao' do + with 'dao' do |*| make :instance, { :instance_type => 'digital_object' } do |instance| @@ -786,7 +779,7 @@ def make_nested_note(note_name, tag) end - with 'daodesc' do + with 'daodesc' do |*| make :note_digital_object, { :type => 'note', :persistent_id => att('id'), @@ -797,7 +790,7 @@ def make_nested_note(note_name, tag) end end - with 'daogrp' do + with 'daogrp' do |*| title = att('title') unless title diff --git a/backend/app/converters/lib/parse_queue.rb b/backend/app/converters/lib/parse_queue.rb index 186d07bab1..668d7916cf 100644 --- a/backend/app/converters/lib/parse_queue.rb +++ b/backend/app/converters/lib/parse_queue.rb @@ -53,6 +53,16 @@ def working_area def <<(obj) self.class.dedupe_subrecords(obj) + + raise "Imported object can't be nil!" unless obj + + # If the record's JSON schema contains a URI (i.e. this is a top-level + # record), then blow up if it isn't provided. + if obj.class.is_a?(JSONModelType) && obj.class.schema['uri'] && !obj.uri + Log.debug("Can't import object: #{obj.inspect}") + raise "Imported object must have a URI!" + end + @working_area.push(obj) end diff --git a/backend/app/converters/lib/xml_sax.rb b/backend/app/converters/lib/xml_sax.rb index 648eeb1b2a..86e710bf6c 100644 --- a/backend/app/converters/lib/xml_sax.rb +++ b/backend/app/converters/lib/xml_sax.rb @@ -31,8 +31,8 @@ def and_in_closing(path, &block) end def ignore(path) - with(path) { @ignore = true } - and_in_closing(path) { @ignore = false } + with(path) {|*| @ignore = true } + and_in_closing(path) {|*| @ignore = false } end @@ -168,10 +168,10 @@ def handle_text(node) def handle_closer(node) @node_shadow = nil @empty_node = false + node_info = node.is_a?(Array) ? node : [node.local_name, node.depth] - + if self.respond_to?("_closing_#{@node_name}") - $stderr.puts "HI!" self.send("_closing_#{@node_name}", node) end diff --git a/backend/app/converters/marcxml_converter.rb b/backend/app/converters/marcxml_converter.rb index 5e10df7477..9502ff5c8f 100644 --- a/backend/app/converters/marcxml_converter.rb +++ b/backend/app/converters/marcxml_converter.rb @@ -46,11 +46,6 @@ def self.for_subjects_and_agents_only(input_file) end - def self.profile - "Convert MARC XML To ArchivesSpace JSONModel records" - end - - def self.configure super do |config| config.doc_frag_nodes << 'record' diff --git a/backend/app/lib/aspace_json_to_managed_container_mapper.rb b/backend/app/lib/aspace_json_to_managed_container_mapper.rb index 2f13e8d47b..8beaeb4fd7 100644 --- a/backend/app/lib/aspace_json_to_managed_container_mapper.rb +++ b/backend/app/lib/aspace_json_to_managed_container_mapper.rb @@ -146,10 +146,10 @@ def try_matching_indicator_within_record(container) def try_matching_indicator_within_collection(container) indicator = container['indicator_1'] - + type_type_id = Enumeration.filter( :name => 'container_type' ).get(:id) type_id = EnumerationValue.filter( :enumeration_id => type_type_id, :value => container["type_1"] ).get(:id) - + return nil if !type_id resource_uri = @json['resource'] && @json['resource']['ref'] @@ -157,19 +157,25 @@ def try_matching_indicator_within_collection(container) resource_id = JSONModel(:resource).id_for(resource_uri) - matching_top_containers = TopContainer.linked_instance_ds. - join(:archival_object, :id => :instance__archival_object_id). - where { Sequel.|({:archival_object__root_record_id => resource_id}, - {:instance__resource_id => resource_id}) }. - filter(:top_container__indicator => indicator). - filter(:top_container__type_id => type_id). - select(:top_container_id) - - TopContainer[:id => matching_top_containers] + matching_top_containers_by_instance = + TopContainer.linked_instance_ds. + join(:archival_object, :id => :instance__archival_object_id). + filter(:instance__resource_id => resource_id). + filter(:top_container__indicator => indicator). + filter(:top_container__type_id => type_id). + select_all(:top_container) + + matching_top_containers_by_ao = + TopContainer.linked_instance_ds. + join(:archival_object, :id => :instance__archival_object_id). + filter(:archival_object__root_record_id => resource_id). + filter(:top_container__indicator => indicator). + filter(:top_container__type_id => type_id). + select_all(:top_container) + + matching_top_containers_by_instance.first || matching_top_containers_by_ao.first end - - def ensure_harmonious_values(top_container, aspace_container) properties = {:indicator => 'indicator_1', :barcode => 'barcode_1', :type_id => 'type_id'} diff --git a/backend/app/lib/background_job_queue.rb b/backend/app/lib/background_job_queue.rb index e9ea69f3e8..928bb03101 100644 --- a/backend/app/lib/background_job_queue.rb +++ b/backend/app/lib/background_job_queue.rb @@ -3,68 +3,75 @@ require 'thread' require 'atomic' + require_relative 'job_runner' -require_relative 'find_and_replace_runner' -require_relative 'print_to_pdf_runner' -require_relative 'reports_runner' -require_relative 'batch_import_runner' -require_relative 'container_conversion_runner' + +# load job runners +Dir.glob(File.join(File.dirname(__FILE__), "job_runners", "*.rb")).sort.each do |file| + require file +end + +# and also from plugins +ASUtils.find_local_directories('backend').each do |prefix| + Dir.glob(File.join(prefix, "job_runners", "*.rb")).sort.each do |file| + require File.absolute_path(file) + end +end + class BackgroundJobQueue JOB_TIMEOUT_SECONDS = AppConfig[:job_timeout_seconds].to_i - def find_stale_job - DB.open do |db| - stale_job = Job.any_repo. - filter(:status => "running"). - where { - system_mtime <= (Time.now - JOB_TIMEOUT_SECONDS) - }.first - - if stale_job - begin - stale_job.time_started = Time.now - stale_job.save - return stale_job - rescue - # If we failed to save the job, another thread must have grabbed it - # first. - nil - end + def get_next_job + # First cancel any jobs that are in a running state but which haven't + # been touched by their watchdog for a time greater than the configured timeout. + # This shouldn't really happen, but his replaces the concept of a stale job + # used in an earlier implementation that was problematic because it could end up + # calling the #run method on a job more than once. + begin + Job.running_jobs_untouched_since(Time.now - JOB_TIMEOUT_SECONDS).each do |job| + job.finish!(:canceled) end + rescue Sequel::NoExistingObject + Log.debug("Another thread cancelled unwatched job #{job.id}, nothing to do on #{Thread.current[:name]}") + rescue => e + Log.error("Error trying to cancel unwatched jobs on #{Thread.current[:name]}: #{e.class} #{$!} #{$@}") end - end + DB.open do |db| + Job.queued_jobs.each do |job| + runner = JobRunner.registered_runner_for(job.type) - def find_queued_job - while true - DB.open do |db| + begin + unless runner + Log.error("No runner registered for #{job.type} job #{job.id}! " + + "Marking as canceled on #{Thread.current[:name]}") - job = Job.any_repo. - filter(:status => "queued"). - order(:time_submitted).first + job.finish!(:canceled) + next + end - return unless job + if !runner.run_concurrently && Job.any_running?(job.type) + Log.debug("Job type #{job.type} is not registered to run concurrently " + + "and there's currently one running, so skipping job #{job.id} " + + "on #{Thread.current[:name]}") + next + end - begin - job.status = "running" - job.time_started = Time.now - job.save + # start the job here to prevent other threads from grabbing it + job.start! return job - rescue - # Someone got this job. - Log.info("Skipped job: #{job}") - sleep 2 + + rescue Sequel::NoExistingObject + # Another thread handled this job. + Log.info("Another thread is handling job #{job.id}, skipping on #{Thread.current[:name]}") end end end - end - - - def get_next_job - find_stale_job || find_queued_job + # No jobs to run at this time + false end @@ -75,16 +82,17 @@ def run_pending_job finished = Atomic.new(false) job_canceled = Atomic.new(false) + job_thread_name = Thread.current[:name] watchdog_thread = Thread.new do while !finished.value DB.open do |db| - Log.debug("Running job #{job.class.to_s}:#{job.id}") + Log.debug("Running job #{job.id} on #{job_thread_name}") job = job.class.any_repo[job.id] if job.status === "canceled" - # Notify the running import that we've been manually canceled - Log.info("Received cancel request for job #{job.id}") + # Notify the running job that we've been manually canceled + Log.info("Received cancel request for job #{job.id} on #{job_thread_name}") job_canceled.value = true end @@ -96,7 +104,12 @@ def run_pending_job end begin - runner = JobRunner.for(job).canceled(job_canceled) + runner = JobRunner.for(job) + + # Give the runner a ref to the canceled atomic, + # so it can find out if it's been canceled + runner.cancelation_signaler(job_canceled) + runner.add_success_hook do # Upon success, have the job set our status to "completed" at the right # point. This allows the batch import to set the job status within the @@ -107,7 +120,7 @@ def run_pending_job finished.value = true watchdog_thread.join - job.finish(:completed) + job.finish!(:completed) end runner.run @@ -117,7 +130,7 @@ def run_pending_job if job_canceled.value # Mark the job as permanently canceled - job.finish(:canceled) + job.finish!(:canceled) else unless job.success? # If the job didn't record success, mark it as finished ourselves. @@ -127,54 +140,62 @@ def run_pending_job # prior to this point, the job might have finished successfully # without being recorded as such. # - Log.warn("Job #{job.id} finished successfully but didn't report success. Marking it as finished successfully ourselves.") + Log.warn("Job #{job.id} finished successfully but didn't report success. " + + "Marking it as finished successfully ourselves, on #{job_thread_name}.") runner.success! end end - rescue - Log.error("Job #{job.id} failed: #{$!} #{$@}") + rescue => e + Log.error("Job #{job.id} on #{job_thread_name} failed: #{e.class} #{$!} #{$@}") # If anything went wrong, make sure the watchdog thread still stops. finished.value = true watchdog_thread.join unless job.success? - job.finish(:failed) + job.finish!(:failed) end end - Log.debug("Completed job #{job.class.to_s}:#{job.id}") + Log.debug("Completed job #{job.id} on #{job_thread_name}") end - def start_background_thread + def start_background_thread(thread_number) Thread.new do + Thread.current[:name] = "background job thread #{thread_number} (#{Thread.current.object_id})" + Log.info("Starting #{Thread.current[:name]}") while true begin run_pending_job - rescue - Log.error("Error in job manager thread: #{$!} #{$@}") + rescue => e + Log.error("Error in #{Thread.current[:name]}: #{e.class} #{$!} #{$@}") end - sleep AppConfig[:job_poll_seconds].to_i end end end + def start_background_threads + AppConfig[:job_thread_count].to_i.times do |i| + start_background_thread(i+1) + end + end + + def self.init - # clear out stale jobs on start + # cancel jobs left in a running state from a previous run begin - while(true) do - stale = find_stale_job - stale.finish(:canceled) - break if stale.nil? + Job.running_jobs_untouched_since(Time.now - JOB_TIMEOUT_SECONDS).each do |job| + job.finish!(:canceled) end - rescue + rescue => e + Log.error("Error trying to cancel old jobs: #{e.class} #{$!} #{$@}") end queue = BackgroundJobQueue.new - queue.start_background_thread + queue.start_background_threads end end diff --git a/backend/app/lib/bootstrap.rb b/backend/app/lib/bootstrap.rb index 25ac59b682..809a0858cf 100644 --- a/backend/app/lib/bootstrap.rb +++ b/backend/app/lib/bootstrap.rb @@ -63,7 +63,7 @@ def self.demo_db? def self.download_demo_db - if File.exists?(File.join(Dir.tmpdir, 'data')) + if File.exist?(File.join(Dir.tmpdir, 'data')) puts "Data directory already exists at #{File.join(Dir.tmpdir, 'data')}." AppConfig[:data_directory] = File.join(Dir.tmpdir, 'data') return @@ -77,7 +77,7 @@ def self.download_demo_db end end - if File.exists?(zip_file) + if File.exist?(zip_file) puts "Extracting data to #{Dir.tmpdir} directory" Zip::File.open(zip_file) do |zf| zf.each do |entry| diff --git a/backend/app/lib/bootstrap_access_control.rb b/backend/app/lib/bootstrap_access_control.rb index 1d9b749273..0f989f8350 100644 --- a/backend/app/lib/bootstrap_access_control.rb +++ b/backend/app/lib/bootstrap_access_control.rb @@ -167,6 +167,14 @@ def self.set_up_base_permissions "The ability to cancel a queued or running importer job", :level => "repository") + Permission.define("create_job", + "The ability to create background jobs", + :level => "repository") + + Permission.define("cancel_job", + "The ability to cancel background jobs", + :level => "repository") + # Updates and deletes to locations, subjects and agents are a bit funny: they're # global objects, but users are granted permission to modify them by being diff --git a/backend/app/lib/component_transfer.rb b/backend/app/lib/component_transfer.rb index a9d05a968a..ee812c8efb 100644 --- a/backend/app/lib/component_transfer.rb +++ b/backend/app/lib/component_transfer.rb @@ -36,13 +36,12 @@ def self.transfer(target_resource_uri, archival_object_uri) # available top-level slot in the target json = obj.class.to_jsonmodel(obj) - json.parent = nil - source_resource_uri = json['resource']['ref'] json.resource['ref'] = target_resource_uri - - obj.update_from_json(json, {}, false) + json.parent = nil + + obj.update_from_json(json, {:force_reposition => true}, false) # generate an event to mark this component transfer event = Event.for_component_transfer(archival_object_uri, source_resource_uri, target_resource_uri) diff --git a/backend/app/lib/container_conversion_runner.rb b/backend/app/lib/container_conversion_runner.rb deleted file mode 100644 index 7230334688..0000000000 --- a/backend/app/lib/container_conversion_runner.rb +++ /dev/null @@ -1,22 +0,0 @@ -require_relative 'job_runner' - - -# This is actually a big bunch of nothing. -# We run this job in the conversation process -class ContainerConversionRunner < JobRunner - - - - def self.instance_for(job) - if job.job_type == "container_conversion_job" - self.new(job) - else - nil - end - end - - def run - super - end - -end diff --git a/backend/app/lib/container_management_conversion.rb b/backend/app/lib/container_management_conversion.rb index c5cbb5a413..1377ad0570 100644 --- a/backend/app/lib/container_management_conversion.rb +++ b/backend/app/lib/container_management_conversion.rb @@ -311,7 +311,7 @@ def convert @job.add_file(file) @job.write_output("Finished container conversion for repository #{repo.id}") - @job.finish(:completed) + @job.finish!(:completed) end end diff --git a/backend/app/lib/crud_helpers.rb b/backend/app/lib/crud_helpers.rb index 99a227825f..cd23cea4a5 100644 --- a/backend/app/lib/crud_helpers.rb +++ b/backend/app/lib/crud_helpers.rb @@ -99,7 +99,10 @@ def with_record_conflict_reporting(model, json) def listing_response(dataset, model) objs = dataset.respond_to?(:all) ? dataset.all : dataset - jsons = model.sequel_to_jsonmodel(objs).map {|json| + + opts = {:calculate_linked_repositories => current_user.can?(:index_system)} + + jsons = model.sequel_to_jsonmodel(objs, opts).map {|json| if json.is_a?(JSONModelType) json.to_hash(:trusted) else diff --git a/backend/app/lib/exceptions.rb b/backend/app/lib/exceptions.rb index fa2449565f..45c5526cbd 100644 --- a/backend/app/lib/exceptions.rb +++ b/backend/app/lib/exceptions.rb @@ -122,6 +122,10 @@ def self.included(base) json_response({:error => "Repository not empty"}, 409) end + error Sinatra::NotFound do + json_response({:error => request.env['sinatra.error']}, 404) + end + error NotFoundException do json_response({:error => request.env['sinatra.error']}, 404) end diff --git a/backend/app/lib/job_runner.rb b/backend/app/lib/job_runner.rb index 27cb5e1e8b..a403a335b0 100644 --- a/backend/app/lib/job_runner.rb +++ b/backend/app/lib/job_runner.rb @@ -1,27 +1,92 @@ class JobRunner class JobRunnerNotFound < StandardError; end - + class JobRunnerError < StandardError; end class BackgroundJobError < StandardError; end + RegisteredRunner = Struct.new(:type, + :runner, + :hidden, + :run_concurrently, + :create_permissions, + :cancel_permissions) + + # In your subclass register your interest in handling job types like this: + # + # class MyRunner < JobRunner + # register_for_job_type('my_job') + # ... + # + # This can be called many times if your runner can handle ore than one job type. + # The type is the jsonmodel_type of the defined schema for the job type. + # + # If another runner has already registered for the type an exception will be thrown. + # + # The register_for_job_type method can take the following options: + # Example: + # register_for_job_type('my_job', + # :hidden => true, + # :run_concurrently => true, + # :create_permissions => :some_perm, + # :cancel_permissions => [:a_perm, :another_perm]) + # + # :hidden - if true then the job_type is not included in the list of job_types + # :run_concurrently - if true then jobs of this type will be run concurrently + # :create_permissions - a permission or list of permissions required to + # create jobs of this type + # :cancel_permissions - a permission or list of permissions required to + # cancel jobs of this type + # + # :hidden and :run_concurrently default to false. + # :create_permissions and :cancel_permissions default to an empty array + + + # Implement this in your subclass + # + # This is the method that does the actual work + def run + raise JobRunnerError.new("#{self.class} must implement the #run method") + end + + + # Nothing below here needs to be implemented in your subclass + + + def self.register_for_job_type(type, opts = {}) + @@runners ||= {} + if @@runners.has_key?(type) + raise JobRunnerError.new("Attempting to register #{self} for job type #{type} " + + "- already handled by #{@@runners[type]}") + end + + @@runners[type] = RegisteredRunner.new(type, + self, + opts.fetch(:hidden, false), + opts.fetch(:run_concurrently, false), + [opts.fetch(:create_permissions, [])].flatten, + [opts.fetch(:cancel_permissions, [])].flatten) + end + + def self.for(job) - @runners.each do |runner| - runner = runner.instance_for(job) - return runner if runner + type = ASUtils.json_parse(job[:job_blob])['jsonmodel_type'] + + unless @@runners.has_key?(type) + raise JobRunnerNotFound.new("No suitable runner found for #{type}") end - raise JobRunnerNotFound.new("No suitable runner found for #{job.job_type}") + @@runners[type].runner.new(job) end - def self.register_runner(subclass) - @runners ||= [] - @runners.unshift(subclass) + def self.registered_runner_for(type) + @@runners.fetch(type, false) end - def self.inherited(subclass) - JobRunner.register_runner(subclass) + def self.registered_job_types + Hash[ @@runners.reject{|k,v| v[:hidden] }.map { |k, v| [k, {:create_permissions => v.create_permissions, + :cancel_permissions => v.cancel_permissions}] } ] end @@ -46,12 +111,13 @@ def success! end - def canceled(canceled) - @job_canceled = canceled - self + def canceled? + @job_canceled.value end - def run; end + def cancelation_signaler(canceled) + @job_canceled = canceled + end end diff --git a/backend/app/lib/batch_import_runner.rb b/backend/app/lib/job_runners/batch_import_runner.rb similarity index 94% rename from backend/app/lib/batch_import_runner.rb rename to backend/app/lib/job_runners/batch_import_runner.rb index 50a81f852e..f31921c701 100644 --- a/backend/app/lib/batch_import_runner.rb +++ b/backend/app/lib/job_runners/batch_import_runner.rb @@ -1,16 +1,14 @@ # prepare an import job run: orchestrates converting the input file's records, # runs the job and gathers its log output, handling any errors. -require_relative 'job_runner' - -[File.expand_path("..", File.dirname(__FILE__)), +[File.expand_path(File.join('..', '..'), File.dirname(__FILE__)), *ASUtils.find_local_directories('backend')].each do |prefix| Dir.glob(File.join(prefix, "converters", "*.rb")).sort.each do |file| require File.absolute_path(file) end end -require_relative 'streaming_import' +require_relative '../streaming_import' class Ticker @@ -41,13 +39,8 @@ def tick_estimate=(n) class BatchImportRunner < JobRunner - def self.instance_for(job) - if job.job_type == "import_job" - self.new(job) - else - nil - end - end + register_for_job_type('import_job', :create_permissions => :import_records, + :cancel_permissions => :cancel_importer_job) def run @@ -155,6 +148,7 @@ def run end else ticker.log("Error: #{CGI.escapeHTML( last_error.inspect )}") + Log.exception(last_error) end ticker.log("!" * 50 ) raise last_error diff --git a/backend/app/lib/job_runners/container_conversion_runner.rb b/backend/app/lib/job_runners/container_conversion_runner.rb new file mode 100644 index 0000000000..41238075e2 --- /dev/null +++ b/backend/app/lib/job_runners/container_conversion_runner.rb @@ -0,0 +1,15 @@ +# This is actually a big bunch of nothing. +# We run this job in the conversation process +class ContainerConversionRunner < JobRunner + + register_for_job_type('container_conversion_job', + :hidden => true, + :create_permissions => :administer_system, + :cancel_permissions => :administer_system) + + + def run + # nothing + end + +end diff --git a/backend/app/lib/find_and_replace_runner.rb b/backend/app/lib/job_runners/find_and_replace_runner.rb similarity index 94% rename from backend/app/lib/find_and_replace_runner.rb rename to backend/app/lib/job_runners/find_and_replace_runner.rb index 06e4ba3bd7..64ac6d2e8e 100644 --- a/backend/app/lib/find_and_replace_runner.rb +++ b/backend/app/lib/job_runners/find_and_replace_runner.rb @@ -1,20 +1,11 @@ -require_relative 'job_runner' - class FindAndReplaceRunner < JobRunner - - def self.instance_for(job) - if job.job_type == "find_and_replace_job" - self.new(job) - else - nil - end - end + register_for_job_type('find_and_replace_job', + :create_permissions => :manage_repository, + :cancel_permissions => :manage_repository) def run - super - job_data = @json.job terminal_error = nil diff --git a/backend/app/lib/print_to_pdf_runner.rb b/backend/app/lib/job_runners/print_to_pdf_runner.rb similarity index 87% rename from backend/app/lib/print_to_pdf_runner.rb rename to backend/app/lib/job_runners/print_to_pdf_runner.rb index ae58d05f55..c74dee5b35 100644 --- a/backend/app/lib/print_to_pdf_runner.rb +++ b/backend/app/lib/job_runners/print_to_pdf_runner.rb @@ -1,23 +1,14 @@ -require_relative 'job_runner' -require_relative "../exporters/lib/exporter" -require_relative 'AS_fop' +require_relative "../../exporters/lib/exporter" +require_relative '../AS_fop' class PrintToPDFRunner < JobRunner include JSONModel - - def self.instance_for(job) - if job.job_type == "print_to_pdf_job" - self.new(job) - else - nil - end - end + register_for_job_type('print_to_pdf_job') def run - super begin RequestContext.open( :repo_id => @job.repo_id) do diff --git a/backend/app/lib/reports_runner.rb b/backend/app/lib/job_runners/reports_runner.rb similarity index 75% rename from backend/app/lib/reports_runner.rb rename to backend/app/lib/job_runners/reports_runner.rb index 38df18e35f..23c46c2f11 100644 --- a/backend/app/lib/reports_runner.rb +++ b/backend/app/lib/job_runners/reports_runner.rb @@ -1,26 +1,20 @@ -require_relative 'job_runner' -require_relative 'reports/report_response' -require_relative 'reports/report_helper' +require_relative '../reports/report_response' +require_relative '../reports/report_helper' require 'json' + class ReportRunner < JobRunner include JSONModel + register_for_job_type('report_job') - def self.instance_for(job) - if job.job_type == "report_job" - self.new(job) - else - nil - end - end def self.reports ReportManager.registered_reports end + def run - super @job.write_output("Generating report") file = ASUtils.tempfile("report_job_") begin @@ -37,16 +31,18 @@ def run params[:repo_id] = @json.repo_id report = ReportRunner.reports[job_data['report_type']] - report_model = report[:model] + report_model = report[:model] + output = DB.open do |db| + ReportResponse.new(report_model.new(params, @job, db)).generate + end - output = ReportResponse.new(report_model.new(params, @job)).generate if output.respond_to? :string file.write(output.string) elsif output.respond_to? :each - output.each do |chunk| - file.write(chunk) - end + output.each do |chunk| + file.write(chunk) + end else file.write(output) end diff --git a/backend/app/lib/reports/report_response.rb b/backend/app/lib/reports/report_response.rb index 25f757fb9a..0dafaaf6fe 100644 --- a/backend/app/lib/reports/report_response.rb +++ b/backend/app/lib/reports/report_response.rb @@ -4,11 +4,8 @@ require_relative 'pdf_response' require_relative 'html_response' require 'erb' +require 'nokogiri' -# this is a generic wrapper for reports reponses. JasperReports do not -# need a reponse wrapper and can return reports on formats using the to_FORMAT -# convention. "Classic" AS reports need a wrapper to render the report in a -# specific format. class ReportResponse attr_accessor :report @@ -20,18 +17,197 @@ def initialize(report, params = {} ) end def generate - if @report.is_a?(JasperReport) - format = @report.format - String.from_java_bytes( @report.render(format.to_sym, @params) ) + @params[:html_report] ||= proc { + ReportErbRenderer.new(@report, @params).render("report.erb") + } + + format = @report.format + + klass = Object.const_get("#{format.upcase}Response") + klass.send(:new, @report, @params).generate + end + +end + +class ReportErbRenderer + + include ERB::Util + + def initialize(report, params) + @report = report + @params = params + end + + def layout? + @params.fetch(:layout, true) + end + + def t(key) + h(I18n.t("reports.#{@report.code}.#{key}")) + end + + def render(file) + HTMLCleaner.new.clean(ERB.new( File.read(template_path(file)) ).result(binding)) + end + + def format_4part(s) + unless s.nil? + ASUtils.json_parse(s).compact.join('.') + end + end + + def text_section(title, value) + # Sick of typing these out... + template = < +

%s

+ %s + +EOS + + template % [h(title), preserve_newlines(h(value))] + end + + def subreport_section(title, subreport, *subreport_args) + # Sick of typing these out... + template = < +

%s

+ %s + +EOS + + template % [h(title), insert_subreport(subreport, *subreport_args)] + end + + def format_date(date) + unless date.nil? + h(date.to_s) + end + end + + def format_boolean(boolean) + if boolean + "Yes" else - file = File.join( File.dirname(__FILE__), "../../views/reports/report.erb") - @params[:html_report] ||= proc { ERB.new( IO.read( file )).result(@report.get_binding) } - - format = @report.format + "No" + end + end + + def format_number(number) + unless number.nil? + h(sprintf('%.2f', number)) + end + end - klass = Object.const_get("#{format.upcase}Response") - klass.send(:new, @report, @params).generate + def insert_subreport(subreport, params = {}) + # If `subreport` is a class, create an instance. Otherwise, use the + # supplied instance directly. This gives the caller the opportunity to + # construct their own object if desired, without being forced to do so in + # every case. + # + subreport_instance = if subreport.is_a?(AbstractReport) + sureport + elsif subreport.is_a?(Class) + @report.new_subreport(subreport, params) + else + raise "insert_subreport expects first argument to be a Class or an AbstractReport" end + + ReportResponse.new(subreport_instance, :layout => false).generate + end + + def transform_text(s) + return '' if s.nil? + + # The HTML to PDF library doesn't currently support the "break-word" CSS + # property that would let us force a linebreak for long strings and URIs. + # Without that, we end up having our tables chopped off, which makes them + # not-especially-useful. + # + # Newer versions of the library might fix this issue, but it appears that the + # licence of the newer version is incompatible with the current ArchivesSpace + # licence. + # + # So, we wrap runs of characters in their own span tags to give the renderer + # a hint on where to place the line breaks. Pretty terrible, but it works. + # + if @report.format === 'pdf' + escaped = h(s) + + # Exciting regexp time! We break our string into "tokens", which are either: + # + # - A single whitespace character + # - A HTML-escaped character (like '&') + # - A run of between 1 and 5 letters + # + # Each token is then wrapped in a span, ensuring that we don't go too + # long without having a spot to break a word if needed. + # + escaped.scan(/[\s]|&.*;|[^\s]{1,5}/).map {|token| + if token.start_with?("&") || token =~ /\A[\s]\Z/ + # Don't mess with & and friends, nor whitespace + token + else + "#{token}" + end + }.join("") + else + h(s) + end + end + + def preserve_newlines(s) + transform_text(s).gsub(/(?:\r\n)+/,"
"); + end + + def template_path(template_name) + if File.exist?(File.join('app', 'views', 'reports', template_name)) + return File.join('app', 'views', 'reports', template_name) + end + + StaticAssetFinder.new('reports').find(template_name) + end + + class HTMLCleaner + + def clean(s) + doc = Nokogiri::HTML(s) + + # Remove empty dt/dd pairs + doc.css("dl").each do |definition| + definition.css('dt, dd').each_slice(2) do |dt, dd| + if dd.text().strip.empty? + dt.remove + dd.remove + end + end + end + + # Remove empty dls + doc.css("dl").each do |dl| + if dl.text().strip.empty? + dl.remove + end + end + + # Remove empty tables + doc.css("table").each do |table| + if table.css("td").empty? + table.remove + end + end + + # Remove empty sections + doc.css("section").each do |section| + if section.children.all? {|elt| elt.is_a?(Nokogiri::XML::Comment) || elt.text.strip.empty? || elt.name == 'h3'} + section.remove + end + end + + doc.to_xhtml + end + end end diff --git a/backend/app/lib/resequencer.rb b/backend/app/lib/resequencer.rb deleted file mode 100644 index b6a300168e..0000000000 --- a/backend/app/lib/resequencer.rb +++ /dev/null @@ -1,64 +0,0 @@ -# -# This will resquence tree_nodes, which can cause problems if the object loses it Sequence -# ( this can happen in transfers and whatnot ) - -require 'atomic' - -class Resequencer - - @running = Atomic.new(false) - @@klasses = [ :ArchivalObject, :DigitalObjectComponent, :ClassificationTerm] - - - class << self - - def running? - @running - end - - def resequence(klass) - repos = Repository.dataset.select(:id).map {|rec| rec[:id]} - repos.each do |r| - DB.attempt { - Kernel.const_get(klass).resequence(r) - }.and_if_constraint_fails { - return - # if there's a failure, just keep going. - } - end - end - - def resequence_all - @@klasses.each do |klass| - self.resequence(klass) - end - end - - - def run(klasses = @@klasses ) - $stderr.puts "*" * 100 - $stderr.puts "*\t\t STARTING RESEQUENCER \t\t*" - $stderr.puts "*\t\t This is a utility that will organize and compact objects trees to ensure their validity. \t\t *" - $stderr.puts "*\t\t Duration of this process depends on the size of your data. \t\t *" - $stderr.puts "*\t\t It should NOT be used on every startup and instead should only be run in rare occassions that require data maintance. \t\t *" - $stderr.puts "*" * 100 - begin - @running.update { |r| r = true } - klasses = klasses & @@klasses - $stderr.puts klasses - klasses.each do |klass| - $stderr.puts klass - self.resequence(klass) - end - ensure - @running.update { |r| r = false } - end - $stderr.puts "*" * 100 - $stderr.puts "*\t\t RESEQUENCER COMPLETE \t\t*" - $stderr.puts "*\t\t Be sure to set the AppConfig[:resequence_on_startup] to false to ensure quicker startups in the future. \t\t*" - $stderr.puts "*" * 100 - - end - - end -end diff --git a/backend/app/lib/rest.rb b/backend/app/lib/rest.rb index fb50ed1b0f..c3407b1b5d 100644 --- a/backend/app/lib/rest.rb +++ b/backend/app/lib/rest.rb @@ -70,7 +70,7 @@ class Endpoint } def initialize(method) - @method = method + @methods = ASUtils.wrap(method) @uri = "" @description = "-- No description provided --" @permissions = [] @@ -95,7 +95,7 @@ def self.all { :uri => @uri, :description => @description, - :method => @method, + :method => @methods, :params => @required_params, :paginated => @paginated, :returns => @returns @@ -108,6 +108,7 @@ def self.all def self.get(uri); self.method(:get).uri(uri); end def self.post(uri); self.method(:post).uri(uri); end def self.delete(uri); self.method(:delete).uri(uri); end + def self.get_or_post(uri); self.method([:get, :post]).uri(uri); end def self.method(method); Endpoint.new(method); end # Helpers @@ -160,6 +161,13 @@ def params(*params) end + def deprecated(description = nil) + @deprecated = true + @deprecated_description = description + + self + end + def paginated(val) @paginated = val @@ -175,7 +183,7 @@ def use_transaction(val) def returns(*returns, &block) - raise "No .permissions declaration for endpoint #{@method.to_s.upcase} #{@uri}" if !@has_permissions + raise "No .permissions declaration for endpoint #{@methods.map{|m|m.to_s.upcase}.join('|')} #{@uri}" if !@has_permissions @returns = returns.map { |r| r[1] = @@return_types[r[1]] || r[1]; r } @@ -184,9 +192,11 @@ def returns(*returns, &block) preconditions = @preconditions rp = @required_params paginated = @paginated + deprecated = @deprecated + deprecated_description = @deprecated_description use_transaction = @use_transaction uri = @uri - method = @method + methods = @methods request_context = @request_context_keyvals if ArchivesSpaceService.development? @@ -195,50 +205,80 @@ def returns(*returns, &block) ArchivesSpaceService.instance_eval { new_route = compile(uri) - if @routes[method.to_s.upcase] - @routes[method.to_s.upcase].reject! do |route| - route[0..1] == new_route + methods.each do |method| + if @routes[method.to_s.upcase] + @routes[method.to_s.upcase].reject! do |route| + route[0..1] == new_route + end end end } end - ArchivesSpaceService.send(@method, @uri, {}) do - RequestContext.open(request_context) do - DB.open do |db| - ensure_params(rp, paginated) + methods.each do |method| + ArchivesSpaceService.send(method, @uri, {}) do + if deprecated + Log.warn("\n" + + ("*" * 80) + + "\n*** CALLING A DEPRECATED ENDPOINT: #{method} #{uri}\n" + + (deprecated_description ? ("\n" + deprecated_description) : "") + + "\n" + + ("*" * 80)) end - Log.debug("Post-processed params: #{Log.filter_passwords(params).inspect}") - - RequestContext.put(:repo_id, params[:repo_id]) - RequestContext.put(:is_high_priority, high_priority_request?) - if Endpoint.is_toplevel_request?(env) || Endpoint.is_potentially_destructive_request?(env) - unless preconditions.all? { |precondition| self.instance_eval &precondition } - raise AccessDeniedException.new("Access denied") + RequestContext.open(request_context) do + DB.open do |db| + ensure_params(rp, paginated) end - end - - DB.open((use_transaction == :unspecified) ? true : use_transaction) do - RequestContext.put(:current_username, current_user.username) - - # If the current user is a manager, show them suppressed records - # too. - if RequestContext.get(:repo_id) - if current_user.can?(:index_system) - # Don't mess with the search user - RequestContext.put(:enforce_suppression, false) + + Log.debug("Post-processed params: #{Log.filter_passwords(params).inspect}") + + RequestContext.put(:repo_id, params[:repo_id]) + RequestContext.put(:is_high_priority, high_priority_request?) + + if Endpoint.is_toplevel_request?(env) || Endpoint.is_potentially_destructive_request?(env) + unless preconditions.all? { |precondition| self.instance_eval &precondition } + raise AccessDeniedException.new("Access denied") + end + end + + use_transaction = (use_transaction == :unspecified) ? true : use_transaction + db_opts = {} + + if use_transaction + if methods == [:post] + # Pure POST requests use read committed so that tree position + # updates can be retried with a chance of succeeding (i.e. we + # can read the last committed value when determining our + # position) + db_opts[:isolation_level] = :committed else - RequestContext.put(:enforce_suppression, - !((current_user.can?(:manage_repository) || - current_user.can?(:view_suppressed) || - current_user.can?(:suppress_archival_record)) && - Preference.defaults['show_suppressed'])) + # Anything that might be querying the DB will get repeatable read. + db_opts[:isolation_level] = :repeatable end end - self.instance_eval &block + DB.open(use_transaction, db_opts) do + RequestContext.put(:current_username, current_user.username) + + # If the current user is a manager, show them suppressed records + # too. + if RequestContext.get(:repo_id) + if current_user.can?(:index_system) + # Don't mess with the search user + RequestContext.put(:enforce_suppression, false) + else + RequestContext.put(:enforce_suppression, + !((current_user.can?(:manage_repository) || + current_user.can?(:view_suppressed) || + current_user.can?(:suppress_archival_record)) && + Preference.defaults['show_suppressed'])) + end + end + + self.instance_eval &block + end end end end @@ -414,7 +454,7 @@ def process_declared_params(declared_params, params, known_params, errors) params[name] = request.body end - if not params[name] and not opts[:optional] and not opts[:default] + if not params[name] and !opts[:optional] and !opts.has_key?(:default) errors[:missing] << {:name => name, :doc => doc} else diff --git a/backend/app/lib/static_asset_finder.rb b/backend/app/lib/static_asset_finder.rb index 610105c6a1..15ea06494d 100644 --- a/backend/app/lib/static_asset_finder.rb +++ b/backend/app/lib/static_asset_finder.rb @@ -4,7 +4,7 @@ def initialize(base) static_dir = File.join(ASUtils.find_base_directory, base) @valid_paths = Dir[File.join(static_dir, "**", "*")]. - select {|path| File.exists?(path) && File.file?(path)} + select {|path| File.exist?(path) && File.file?(path)} end diff --git a/backend/app/lib/streaming_import.rb b/backend/app/lib/streaming_import.rb index e96a9ed67f..a8ba361101 100644 --- a/backend/app/lib/streaming_import.rb +++ b/backend/app/lib/streaming_import.rb @@ -405,21 +405,27 @@ def touch_toplevel_records # transaction is committed. # # So, do some sneaky updates here to set the mtimes to right now. + # + # Note: Under Derby (where imports run without transactions), this has a + # pretty good chance of deadlocking with an indexing thread that is + # currently trying to index these records. But since Derby imports aren't + # running within a transaction, we don't care anyway! - records_by_type = {} + if DB.supports_mvcc? + records_by_type = {} - @logical_urls.values.compact.each do |uri| - ref = JSONModel.parse_reference(uri) + @logical_urls.values.compact.each do |uri| + ref = JSONModel.parse_reference(uri) - records_by_type[ref[:type]] ||= [] - records_by_type[ref[:type]] << ref[:id] - end + records_by_type[ref[:type]] ||= [] + records_by_type[ref[:type]] << ref[:id] + end - records_by_type.each do |type, ids| - model = model_for(type) - model.update_mtime_for_ids(ids) + records_by_type.each do |type, ids| + model = model_for(type) + model.update_mtime_for_ids(ids) + end end - end diff --git a/backend/app/main.rb b/backend/app/main.rb index ed94b8e2cf..be74365c5f 100644 --- a/backend/app/main.rb +++ b/backend/app/main.rb @@ -22,11 +22,11 @@ require_relative 'lib/reports/report_helper' require_relative 'lib/component_transfer' require_relative 'lib/progress_ticker' -require_relative 'lib/resequencer' require_relative 'lib/container_management_conversion' require 'solr_snapshotter' require 'barcode_check' +require 'record_inheritance' require 'uri' require 'sinatra/base' @@ -57,15 +57,16 @@ def self.loaded_hook(&block) require 'sinatra/reloader' register Sinatra::Reloader config.also_reload File.join("app", "**", "*.rb") + config.also_reload File.join("..", "plugins", "*", "backend", "**", "*.rb") config.dont_reload File.join("app", "lib", "rest.rb") config.dont_reload File.join("**", "exporters", "*.rb") config.dont_reload File.join("**", "spec", "*.rb") - set :server, :puma + set :server, :mizuno end configure :test do |config| - set :server, :puma + set :server, :mizuno end @@ -97,6 +98,9 @@ def self.loaded_hook(&block) require_relative "model/ASModel" + # Set up our JSON schemas now that we know the JSONModels have been loaded + RecordInheritance.prepare_schemas + # let's check that our migrations have passed and we're on the right # schema_info version unless AppConfig[:ignore_schema_info_check] @@ -118,15 +122,6 @@ def self.loaded_hook(&block) end - if AppConfig[:enable_jasper] && DB.supports_jasper? - require_relative 'model/reports/jasper_report' - require_relative 'model/reports/jasper_report_register' - JasperReport.compile if AppConfig[:compile_jasper] - JasperReportRegister.register_reports - end - - - [File.dirname(__FILE__), *ASUtils.find_local_directories('backend')].each do |prefix| ['model/mixins', 'model', 'model/reports', 'controllers'].each do |path| Dir.glob(File.join(prefix, path, "*.rb")).sort.each do |file| @@ -135,6 +130,12 @@ def self.loaded_hook(&block) end end + # Include packaged reports + Array(StaticAssetFinder.new('reports').find_all(".rb")).each do |report_file| + require File.absolute_path(report_file) + end + + # Start the notifications background delivery thread Notifications.init if ASpaceEnvironment.environment != :unit_test @@ -197,7 +198,7 @@ def self.loaded_hook(&block) # Load plugin init.rb files (if present) ASUtils.find_local_directories('backend').each do |dir| init_file = File.join(dir, "plugin_init.rb") - if File.exists?(init_file) + if File.exist?(init_file) load init_file end end @@ -206,7 +207,6 @@ def self.loaded_hook(&block) Notifications.notify("BACKEND_STARTED") Log.noisiness "Logger::#{AppConfig[:backend_log_level].upcase}" - Resequencer.run( [ :ArchivalObject, :DigitalObjectComponent, :ClassificationTerm ] ) if AppConfig[:resequence_on_startup] # this checks the system_event table to see if we've already run the CMM # for the upgrade from =< v1.4.2 @@ -242,6 +242,9 @@ def high_priority_request? class RequestWrappingMiddleware + + Session.init + def initialize(app) @app = app end @@ -296,7 +299,7 @@ def call(env) end_time = Time.now - Log.debug("Responded with #{result.to_s[0..512]}... in #{(end_time - start_time) * 1000}ms") + Log.debug("Responded with #{result.to_s[0..512]}... in #{((end_time - start_time) * 1000).to_i}ms") result end @@ -336,7 +339,7 @@ def call(env) if $0 == __FILE__ Log.info("Dev server starting up...") - ArchivesSpaceService.run!(:port => (ARGV[0] or 4567)) do |server| + ArchivesSpaceService.run!(:bind => '0.0.0.0', :port => (ARGV[0] or 4567)) do |server| def server.stop # Shutdown long polling threads that would otherwise hold things up. Notifications.shutdown diff --git a/backend/app/model/ASModel_crud.rb b/backend/app/model/ASModel_crud.rb index ae9ea12afb..960c77740e 100644 --- a/backend/app/model/ASModel_crud.rb +++ b/backend/app/model/ASModel_crud.rb @@ -417,10 +417,32 @@ def def_nested_record(opts) opts[:is_array] = true if !opts.has_key?(:is_array) + # Store our association on the nested record's model so we can walk back + # the other way. + ArchivesSpaceService.loaded_hook do + nested_model = Kernel.const_get(opts[:association][:class_name]) + nested_model.add_enclosing_association(opts[:association]) + end + nested_records << opts end + # Record the association of the record that encloses this one. For + # example, an Archival Object encloses an Instance record because an + # Instance is a nested record of an Archival Object. + def add_enclosing_association(association) + @enclosing_associations ||= [] + @enclosing_associations << association + end + + # If this is a nested record, return the list of associations that link us + # back to our parent(s). Top-level records just return an empty list. + def enclosing_associations + @enclosing_associations || [] + end + + def get_nested_graph Hash[nested_records.map {|nested_record| model = Kernel.const_get(nested_record[:association][:class_name]) @@ -505,6 +527,13 @@ def handle_delete(ids_to_delete) end + def update_mtime_for_repo_id(repo_id) + if model_scope == :repository + self.dataset.filter(:repo_id => repo_id).update(:system_mtime => Time.now) + end + end + + def update_mtime_for_ids(ids) now = Time.now ids.each_slice(50) do |subset| diff --git a/backend/app/model/ASModel_scoping.rb b/backend/app/model/ASModel_scoping.rb index bf67d94161..f40e583bb5 100644 --- a/backend/app/model/ASModel_scoping.rb +++ b/backend/app/model/ASModel_scoping.rb @@ -111,7 +111,7 @@ def set_model_scope(value) filter = model.columns.include?(:repo_id) ? {:repo_id => model.active_repository} : {} if model.suppressible? && model.enforce_suppression? - filter[:suppressed] = 0 + filter[Sequel.qualify(model.table_name, :suppressed)] = 0 end orig_ds.filter(filter) @@ -121,7 +121,7 @@ def set_model_scope(value) # And another that will return records from any repository def_dataset_method(:any_repo) do if model.suppressible? && model.enforce_suppression? - orig_ds.filter(:suppressed => 0) + orig_ds.filter(Sequel.qualify(model.table_name, :suppressed) => 0) else orig_ds end diff --git a/backend/app/model/advanced_query_string.rb b/backend/app/model/advanced_query_string.rb new file mode 100644 index 0000000000..139eecc856 --- /dev/null +++ b/backend/app/model/advanced_query_string.rb @@ -0,0 +1,88 @@ +require 'time' + +class AdvancedQueryString + def initialize(query, use_literal) + @query = query + @use_literal = use_literal + end + + def to_solr_s + return empty_solr_s if empty_search? + + "#{prefix}#{field}:#{value}" + end + + private + + def use_literal? + @use_literal + end + + def empty_solr_s + if negated? + if date? + "#{field}:[* TO *]" + else + "#{field}:['' TO *]" + end + else + "(*:* NOT #{field}:*)" + end + end + + def prefix + negated? ? "-" : "" + end + + def field + AdvancedSearch.solr_field_for(@query['field']) + end + + def value + if date? + base_time = Time.parse(@query["value"]).utc.iso8601 + + if @query["comparator"] == "lesser_than" + "[* TO #{base_time}-1MILLISECOND]" + elsif @query["comparator"] == "greater_than" + "[#{base_time}+1DAY TO *]" + else # @query["comparator"] == "equal" + "[#{base_time} TO #{base_time}+1DAY-1MILLISECOND]" + end + elsif @query["jsonmodel_type"] == "range_query" + "[#{@query["from"] || '*'} TO #{@query["to"] || '*'}]" + elsif @query["jsonmodel_type"] == "field_query" && (use_literal? || @query["literal"]) + "(\"#{solr_escape(@query['value'])}\")" + else + "(#{@query['value']})" + end + end + + def empty_search? + if @query["jsonmodel_type"] == "date_field_query" + @query["comparator"] == "empty" + elsif @query["jsonmodel_type"] == "boolean_field_query" + false + elsif @query["jsonmodel_type"] == "field_query" + @query["comparator"] == "empty" + else + raise "Unknown field query type: #{@query["jsonmodel_type"]}" + end + end + + def negated? + @query['negated'] + end + + def date? + @query["jsonmodel_type"] == "date_field_query" + end + + + SOLR_CHARS = '+-&|!(){}[]^"~*?:\\/' + + def solr_escape(s) + pattern = Regexp.quote(SOLR_CHARS) + s.gsub(/([#{pattern}])/, '\\\\\1') + end +end diff --git a/backend/app/model/ancestor_listing.rb b/backend/app/model/ancestor_listing.rb new file mode 100644 index 0000000000..9ed68ea1dd --- /dev/null +++ b/backend/app/model/ancestor_listing.rb @@ -0,0 +1,125 @@ +class AncestorListing + + # For a given set of Sequel `objs` and their corresponding `jsons` records, + # walk back up the tree enumerating their ancestor records and add them as + # refs to the `ancestors` JSON property. + def self.add_ancestors(objs, jsons) + return if objs.empty? + + node_class = objs[0].class + ancestors = new(node_class, objs) + + jsons.zip(objs).each do |json, obj| + json['ancestors'] = [] + + current = obj + while (parent = ancestors.parent_of(current.id)) + json['ancestors'] << {'ref' => parent.uri, + 'level' => parent.level} + current = parent + end + + root = ancestors.root_record(obj[:root_record_id]) + json['ancestors'] << { + 'ref' => root.uri, + 'level' => root.level + } + end + end + + + def initialize(node_class, objs) + @node_model = node_class + @root_model = node_class.root_model + + @ancestors = {} + @root_records = {} + + objs.each do |obj| + @ancestors[obj.id] = {:id => obj.id, + :level => level_value(obj.level, obj.other_level), + :parent_id => obj.parent_id, + :root_record_id => obj.root_record_id} + end + + build_ancestor_links + end + + # The parent of the given node + def parent_of(node_id) + parent_id = @ancestors.fetch(node_id)[:parent_id] + + if parent_id + ancestor = @ancestors.fetch(parent_id) + + Ancestor.new(ancestor[:id], + @node_model.uri_for(@node_model.my_jsonmodel.record_type, ancestor[:id]), + ancestor[:level]) + end + end + + # The root record of the given node + def root_record(root_record_id) + root_record = @root_records.fetch(root_record_id) + + Ancestor.new(root_record[:id], + @root_model.uri_for(@root_model.my_jsonmodel.record_type, root_record[:id]), + root_record[:level]) + end + + private + + # Walk and record the ancestors of all records of interest + def build_ancestor_links + # Starting with our initial set of nodes, walk up the tree parent-by-parent + # until we hit the top-level nodes. + while true + parent_ids_to_fetch = @ancestors.map {|_, ancestor| + if ancestor[:parent_id] && !@ancestors[ancestor[:parent_id]] + ancestor[:parent_id] + end + }.compact + + # Done! + break if parent_ids_to_fetch.empty? + + @node_model + .join(:enumeration_value, :enumeration_value__id => Sequel.qualify(@node_model.table_name, :level_id)) + .filter(Sequel.qualify(@node_model.table_name, :id) => parent_ids_to_fetch) + .select(Sequel.qualify(@node_model.table_name, :id), + :parent_id, + :root_record_id, + Sequel.as(:enumeration_value__value, :level), + :other_level).each do |row| + @ancestors[row[:id]] = { + :id => row[:id], + :level => level_value(row[:level], row[:other_level]), + :parent_id => row[:parent_id], + :root_record_id => row[:root_record_id] + } + end + end + + # Now fetch the root record for each chain of record nodes + root_record_ids = @ancestors.map {|_, ancestor| ancestor[:root_record_id]}.compact.uniq + + @root_model + .join(:enumeration_value, :enumeration_value__id => Sequel.qualify(@root_model.table_name, :level_id)) + .filter(Sequel.qualify(@root_model.table_name, :id) => root_record_ids) + .select(Sequel.qualify(@root_model.table_name, :id), + Sequel.as(:enumeration_value__value, :level), + :other_level).each do |row| + @root_records[row[:id]] = { + :id => row[:id], + :level => level_value(row[:level], row[:other_level]), + } + end + end + + Ancestor = Struct.new(:id, :uri, :level) + + def level_value(level, other_level) + level == 'otherlevel' ? other_level : level + end + +end diff --git a/backend/app/model/archival_object.rb b/backend/app/model/archival_object.rb index 218475abfa..ceda57a6eb 100644 --- a/backend/app/model/archival_object.rb +++ b/backend/app/model/archival_object.rb @@ -1,4 +1,6 @@ require 'securerandom' +require_relative 'ancestor_listing' + class ArchivalObject < Sequel::Model(:archival_object) include ASModel @@ -20,7 +22,6 @@ class ArchivalObject < Sequel::Model(:archival_object) include Events include Publishable include ReindexTopContainers - include ArchivalObjectSeries include RightsRestrictionNotes include MapToAspaceContainer include RepresentativeImages @@ -61,6 +62,13 @@ class ArchivalObject < Sequel::Model(:archival_object) display_string } + + def self.sequel_to_jsonmodel(objs, opts = {}) + jsons = super + AncestorListing.add_ancestors(objs, jsons) + jsons + end + def validate validates_unique([:root_record_id, :ref_id], :message => "An Archival Object Ref ID must be unique to its resource") diff --git a/backend/app/model/classification.rb b/backend/app/model/classification.rb index 0057fefe1a..e8719cea77 100644 --- a/backend/app/model/classification.rb +++ b/backend/app/model/classification.rb @@ -5,6 +5,8 @@ class Classification < Sequel::Model(:classification) include ClassificationIndexing include Publishable + enable_suppression + corresponds_to JSONModel(:classification) set_model_scope(:repository) diff --git a/backend/app/model/classification_term.rb b/backend/app/model/classification_term.rb index 0ae53408a5..85882bb363 100644 --- a/backend/app/model/classification_term.rb +++ b/backend/app/model/classification_term.rb @@ -6,6 +6,9 @@ class ClassificationTerm < Sequel::Model(:classification_term) include TreeNodes include ClassificationIndexing include Publishable + include AutoGenerator + + enable_suppression corresponds_to JSONModel(:classification_term) set_model_scope(:repository) @@ -23,6 +26,11 @@ class ClassificationTerm < Sequel::Model(:classification_term) :json_property => 'linked_records', :contains_references_to_types => proc {[Accession, Resource]}) + auto_generate :property => :display_string, + :generator => proc { |json| + json['title'] + } + def self.create_from_json(json, opts = {}) diff --git a/backend/app/model/db.rb b/backend/app/model/db.rb index 96358baf4d..9f43134244 100644 --- a/backend/app/model/db.rb +++ b/backend/app/model/db.rb @@ -23,247 +23,223 @@ class DB } ] - def self.connect - if not @pool - - if !AppConfig[:allow_unsupported_database] - check_supported(AppConfig[:db_url]) - end + class DBPool - begin - Log.info("Connecting to database: #{AppConfig[:db_url_redacted]}. Max connections: #{AppConfig[:db_max_connections]}") - pool = Sequel.connect(AppConfig[:db_url], - :max_connections => AppConfig[:db_max_connections], - :test => true, - :loggers => (AppConfig[:db_debug_log] ? [Logger.new($stderr)] : []) - ) - - # Test if any tables exist - pool[:schema_info].all - - if pool.database_type == :mysql && AppConfig[:allow_non_utf8_mysql_database] != "true" - ensure_tables_are_utf8(pool) - end + attr_reader :pool_size - @pool = pool - rescue - Log.error("DB connection failed: #{$!}") - end + def initialize(pool_size = AppConfig[:db_max_connections], opts = {}) + @pool_size = pool_size + @opts = opts end - end - - - def self.ensure_tables_are_utf8(db) - - non_utf8_tables = db[:information_schema__tables]. - join(:information_schema__collation_character_set_applicability, :collation_name => :table_collation). - filter(:table_schema => Sequel.function(:database)). - filter(Sequel.~(:character_set_name => 'utf8')).all - - unless (non_utf8_tables.empty?) - msg = < pool_size, + :test => true, + :loggers => (AppConfig[:db_debug_log] ? [Logger.new($stderr)] : []) + ) + + # Test if any tables exist + pool[:schema_info].all + + if !@opts[:skip_utf8_check] && pool.database_type == :mysql && AppConfig[:allow_non_utf8_mysql_database] != "true" + ensure_tables_are_utf8(pool) + end -EOF + @pool = pool + rescue + Log.error("DB connection failed: #{$!}") + end + end - Log.warn(msg) - raise msg + self end - Log.info("All tables checked and confirmed set to UTF-8. Nice job!") - end - def self.connected? - not @pool.nil? - end + def connected? + not @pool.nil? + end - def self.transaction(*args) - @pool.transaction(*args) do - yield + def transaction(*args) + @pool.transaction(*args) do + yield + end end - end - def self.after_commit(&block) - if @pool.in_transaction? - @pool.after_commit do + def after_commit(&block) + if @pool.in_transaction? + @pool.after_commit do + block.call + end + else block.call end - else - block.call end - end - - - def self.session_storage - Thread.current[:db_session_storage] or raise "Not inside transaction!" - end - def self.open(transaction = true, opts = {}) - # Give us a place to hang storage that relates to the current database - # session. - Thread.current[:db_session_storage] ||= {} - Thread.current[:nesting_level] ||= 0 - Thread.current[:nesting_level] += 1 + def session_storage + Thread.current[:db_session_storage] or raise "Not inside transaction!" + end - begin - last_err = false - retries = opts[:retries] || 10 + def open(transaction = true, opts = {}) - retries.times do |attempt| - begin - if transaction - self.transaction do - return yield @pool - end + # Give us a place to hang storage that relates to the current database + # session. + Thread.current[:db_session_storage] ||= {} + Thread.current[:nesting_level] ||= 0 + Thread.current[:nesting_level] += 1 - # Sometimes we'll make it to here. That means we threw a - # Sequel::Rollback which has been quietly caught. - return nil - else - begin - return yield @pool - rescue Sequel::Rollback - # If we're not in a transaction we can't roll back, but no need to blow up. - Log.warn("Sequel::Rollback caught but we're not inside of a transaction") + begin + last_err = false + retries = opts[:retries] || 10 + + retries.times do |attempt| + begin + if transaction + self.transaction(:isolation => opts.fetch(:isolation_level, :repeatable)) do + return yield @pool + end + + # Sometimes we'll make it to here. That means we threw a + # Sequel::Rollback which has been quietly caught. return nil + else + begin + return yield @pool + rescue Sequel::Rollback + # If we're not in a transaction we can't roll back, but no need to blow up. + Log.warn("Sequel::Rollback caught but we're not inside of a transaction") + return nil + end end - end - rescue Sequel::DatabaseDisconnectError => e - # MySQL might have been restarted. - last_err = e - Log.info("Connecting to the database failed. Retrying...") - sleep(opts[:db_failed_retry_delay] || 3) + rescue Sequel::DatabaseDisconnectError => e + # MySQL might have been restarted. + last_err = e + Log.info("Connecting to the database failed. Retrying...") + sleep(opts[:db_failed_retry_delay] || 3) - rescue Sequel::NoExistingObject, Sequel::DatabaseError => e - if (attempt + 1) < retries && is_retriable_exception(e, opts) && transaction - Log.info("Retrying transaction after retriable exception (#{e})") - sleep(opts[:retry_delay] || 1) - else - raise e + rescue Sequel::NoExistingObject, Sequel::DatabaseError => e + if (attempt + 1) < retries && is_retriable_exception(e, opts) && transaction + Log.info("Retrying transaction after retriable exception (#{e})") + sleep(opts[:retry_delay] || 1) + else + raise e + end end - end - if last_err - Log.error("Failed to connect to the database") - Log.exception(last_err) + if last_err + Log.error("Failed to connect to the database") + Log.exception(last_err) - raise "Failed to connect to the database: #{last_err}" + raise "Failed to connect to the database: #{last_err}" + end end - end - ensure - Thread.current[:nesting_level] -= 1 + ensure + Thread.current[:nesting_level] -= 1 - if Thread.current[:nesting_level] <= 0 - Thread.current[:db_session_storage] = nil + if Thread.current[:nesting_level] <= 0 + Thread.current[:db_session_storage] = nil + end end end - end - def self.in_transaction? - @pool.in_transaction? - end + def in_transaction? + @pool.in_transaction? + end - def self.sysinfo - jdbc_metadata.merge(system_metadata) - end + def sysinfo + jdbc_metadata.merge(system_metadata) + end - def self.jdbc_metadata - md = open { |p| p.synchronize { |c| c.getMetaData }} - { "databaseProductName" => md.getDatabaseProductName, - "databaseProductVersion" => md.getDatabaseProductVersion } - end + def jdbc_metadata + md = open { |p| p.synchronize { |c| c.getMetaData }} + { "databaseProductName" => md.getDatabaseProductName, + "databaseProductVersion" => md.getDatabaseProductVersion } + end - def self.system_metadata - RbConfig.const_get("CONFIG").select { |key| ['host_os', 'host_cpu', - 'build', 'ruby_version'].include? key } - end + def system_metadata + RbConfig.const_get("CONFIG").select { |key| ['host_os', 'host_cpu', + 'build', 'ruby_version'].include? key } + end - def self.needs_savepoint? - # Postgres needs a savepoint for any statement that might fail - # (otherwise the whole transaction becomes invalid). Use a savepoint to - # run the happy case, since we're half expecting it to fail. - [:postgres].include?(@pool.database_type) - end + def needs_savepoint? + # Postgres needs a savepoint for any statement that might fail + # (otherwise the whole transaction becomes invalid). Use a savepoint to + # run the happy case, since we're half expecting it to fail. + [:postgres].include?(@pool.database_type) + end - class DBAttempt + class DBAttempt - def initialize(happy_path) - @happy_path = happy_path - end + def initialize(happy_path) + @happy_path = happy_path + end - def and_if_constraint_fails(&failed_path) - begin - DB.transaction(:savepoint => DB.needs_savepoint?) do - @happy_path.call - end - rescue Sequel::DatabaseError => ex - if DB.is_integrity_violation(ex) + def and_if_constraint_fails(&failed_path) + begin + DB.transaction(:savepoint => DB.needs_savepoint?) do + @happy_path.call + end + rescue Sequel::DatabaseError => ex + if DB.is_integrity_violation(ex) + failed_path.call(ex) + else + raise ex + end + rescue Sequel::ValidationFailed => ex failed_path.call(ex) - else - raise ex end - rescue Sequel::ValidationFailed => ex - failed_path.call(ex) end - end - end + end - def self.attempt(&block) - DBAttempt.new(block) - end + def attempt(&block) + DBAttempt.new(block) + end - # Yeesh. - def self.is_integrity_violation(exception) - (exception.wrapped_exception.cause or exception.wrapped_exception).getSQLState() =~ /^23/ - end + # Yeesh. + def is_integrity_violation(exception) + (exception.wrapped_exception.cause or exception.wrapped_exception).getSQLState() =~ /^23/ + end - def self.is_retriable_exception(exception, opts = {}) - # Transaction was rolled back, but we can retry - (exception.instance_of?(RetryTransaction) || - (opts[:retry_on_optimistic_locking_fail] && - exception.instance_of?(Sequel::Plugins::OptimisticLocking::Error)) || - (exception.wrapped_exception && ( exception.wrapped_exception.cause or exception.wrapped_exception).getSQLState() =~ /^(40|41)/) ) - end + def is_retriable_exception(exception, opts = {}) + # Transaction was rolled back, but we can retry + (exception.instance_of?(RetryTransaction) || + (opts[:retry_on_optimistic_locking_fail] && + exception.instance_of?(Sequel::Plugins::OptimisticLocking::Error)) || + (exception.wrapped_exception && ( exception.wrapped_exception.cause or exception.wrapped_exception).getSQLState() =~ /^(40|41)/) ) + end - def self.disconnect - @pool.disconnect - end + def disconnect + @pool.disconnect + end - def self.check_supported(url) - if !SUPPORTED_DATABASES.any? {|db| url =~ db[:pattern]} + def check_supported(url) + if !SUPPORTED_DATABASES.any? {|db| url =~ db[:pattern]} - msg = < obj.id, :lock_version => obj.lock_version). + update(:lock_version => obj.lock_version + 1, + :system_mtime => Time.now) - Log.info("Writing backup to '#{this_backup}'") + if updated_rows != 1 + raise Sequel::Plugins::OptimisticLocking::Error.new("Couldn't create version of: #{obj}") + end + end - @pool.pool.hold do |c| - cs = c.prepare_call("CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)") - cs.set_string(1, this_backup.to_s) - cs.execute - cs.close + + def supports_mvcc? + ![:derby, :h2].include?(@pool.database_type) end - expire_backups - end + def supports_join_updates? + ![:derby, :h2].include?(@pool.database_type) + end - def self.increase_lock_version_or_fail(obj) - updated_rows = obj.class.dataset.filter(:id => obj.id, :lock_version => obj.lock_version). - update(:lock_version => obj.lock_version + 1, - :system_mtime => Time.now) - if updated_rows != 1 - raise Sequel::Plugins::OptimisticLocking::Error.new("Couldn't create version of: #{obj}") + def needs_blob_hack? + (@pool.database_type == :derby) end - end - - def self.supports_jasper? - ![:derby, :h2].include?(@pool.database_type) - end + def blobify(s) + (@pool.database_type == :derby) ? s.to_sequel_blob : s + end - def self.supports_mvcc? - ![:derby, :h2].include?(@pool.database_type) - end + def concat(s1, s2) + if @pool.database_type == :derby + "#{s1} || #{s2}" + else + "CONCAT(#{s1}, #{s2})" + end + end + + + def ensure_tables_are_utf8(db) + + non_utf8_tables = db[:information_schema__tables]. + join(:information_schema__collation_character_set_applicability, :collation_name => :table_collation). + filter(:table_schema => Sequel.function(:database)). + filter(Sequel.~(:character_set_name => 'utf8')).all + + unless (non_utf8_tables.empty?) + msg = < => [#, #], ...} + # + def call(records) + record_id_to_repo_ids = build_id_map(records) + + repositories = Repository.all.group_by(&:id) + + result = {} + records.each do |record| + result[record] = record_id_to_repo_ids + .fetch(record.id, []) + .map {|repo_ids| repositories.fetch(repo_ids)} + .flatten + end + + result + end + + private + + # Build a map from record IDs to the repositories that use them + # + # For example: {subject.id => [repo1.id, repo2.id, ...], subject2.id => [...], ...} + # + def build_id_map(records) + result = {} + + # E.g. subject_rlshp.subject_id + global_model_column = @relationship.reference_columns_for(@global_record_model).first + + @relationship.participating_models.each do |linked_repo_model| + # We're only interested in record types that belong to a repo + next unless linked_repo_model.model_scope == :repository + + # Since we're only working with relationships that link a global record to + # a repo-scoped record, we don't have to worry about relationships between + # records of the same type, so there'll only ever be one column to check. + linked_repo_model_column = @relationship.reference_columns_for(linked_repo_model).first + + # E.g. join archival_object to subject_rlshp + ds = linked_repo_model + .any_repo + .join(@relationship.table_name, + linked_repo_model_column => Sequel.qualify(linked_repo_model.table_name, :id)) + + # Limit to the objects we care about and map out the linkages from our + # records to the repositories that use them. + ds.filter(global_model_column => records.map(&:id)) + .select(global_model_column, Sequel.qualify(linked_repo_model.table_name, :repo_id)) + .distinct + .each do |row| + global_record_id = row[global_model_column] + repo_id = row[:repo_id] + + result[global_record_id] ||= Set.new + result[global_record_id] << repo_id + end + end + + result + end +end diff --git a/backend/app/model/job.rb b/backend/app/model/job.rb index 89f8b60dc3..95b22151ab 100644 --- a/backend/app/model/job.rb +++ b/backend/app/model/job.rb @@ -76,9 +76,13 @@ def self.create_from_json(json, opts = {}) if json.job_params == "null" json.job_params = "" end - + + # force a validation on the job + job = JSONModel(json.job['jsonmodel_type'].intern).from_hash(json.job) + super(json, opts.merge(:time_submitted => Time.now, :owner_id => opts.fetch(:user).id, + :job_type => json.job['jsonmodel_type'], :job_blob => ASUtils.to_json(json.job), :job_params => ASUtils.to_json(json.job_params) )) @@ -88,17 +92,47 @@ def self.create_from_json(json, opts = {}) def self.sequel_to_jsonmodel(objs, opts = {}) jsons = super jsons.zip(objs).each do |json, obj| - json.job = JSONModel(json.job_type.intern).from_json(obj.job_blob) + json.job = JSONModel(obj.type.intern).from_hash(obj.job) json.owner = obj.owner.username - json.queue_position = obj.queue_position if obj.status === "queued" + json.queue_position = obj.queue_position if obj.status === 'queued' end jsons end + def self.queued_jobs + self.any_repo.filter(:status => 'queued').order(:time_submitted) + end + + + def self.running_jobs + self.any_repo.filter(:status => 'running').order(:time_submitted) + end + + + def self.running_jobs_untouched_since(time) + self.any_repo.filter(:status => "running").where { system_mtime < time } + end + + + def self.any_running?(type) + !self.any_repo.filter(:status => 'running').where(:job_type => type).empty? + end + + + def job + @job ||= ASUtils.json_parse(job_blob) + end + + + def type + self.job_type + end + + def file_store - @file_store ||= JobFileStore.new("#{job_type}_#{id}") + @file_store ||= JobFileStore.new("#{type}_#{id}") end @@ -143,11 +177,18 @@ def queue_position end - def finish(status) + def start! + self.status = 'running' + self.time_started = Time.now + self.save + end + + + def finish!(status) file_store.close_output self.reload - self.status = "#{status}" + self.status = [:canceled, :failed].include?(status) ? status.to_s : 'completed' self.time_finished = Time.now self.save end diff --git a/backend/app/model/large_tree.rb b/backend/app/model/large_tree.rb new file mode 100644 index 0000000000..3b69d39b74 --- /dev/null +++ b/backend/app/model/large_tree.rb @@ -0,0 +1,299 @@ +# What's the big idea? +# +# ArchivesSpace has some big trees in it, and sometimes they look a lot like big +# sticks. Back in the dark ages, we used JSTree for our trees, which in general +# is perfectly cromulent. We recognized the risk of having some very large +# collections, so dutifully configured JSTree to lazily load subtrees as the +# user expanded them (avoiding having to load the full tree into memory right +# away). +# +# However, time makes fools of us all. The JSTree approach works fine if your +# tree is fairly well balanced, but that's not what things look like in the real +# world. Some trees have a single root node and tens of thousands of records +# directly underneath it. Lazy loading at the subtree level doesn't save you +# here: as soon as you expand that (single) node, you're toast. +# +# This "large tree" business is a way around all of this. It's effectively a +# hybrid of trees and pagination, except we call the pages "waypoints" for +# reasons known only to me. So here's the big idea: +# +# * You want to show a tree. You ask the API to give you the root node. +# +# * The root node tells you whether or not it has children, how many children, +# and how many waypoints that works out to. +# +# * Each waypoint is a fixed-size page of nodes. If the waypoint size is set +# to 200, a node with 1,000 children would have 5 waypoints underneath it. +# +# * So, to display the records underneath the root node, you fetch the root +# node, then fetch the first waypoint to get the first N nodes. If you need +# to show more nodes (i.e. if the user has scrolled down), you fetch the +# second waypoint, and so on. +# +# * The records underneath the root might have their own children, and they'll +# have their own waypoints that you can fetch in the same way. It's nodes, +# waypoints and turtles the whole way down. +# +# All of this interacts with the largetree.js code in the staff and public +# interfaces. You open a resource record, and largetree.js fetches the root +# node and inserts placeholders for each waypoint underneath it. As the user +# scrolls towards a placeholder, the code starts building tracks ahead of the +# train, fetching that waypoint and rendering the records it contains. When a +# user expands a node to view its children, that process repeats again (the node +# is fetched, waypoint placeholders inserted, etc.). +# +# The public interface runs the same code as the staff interface, but with a +# small twist: it fetches its nodes and waypoints from Solr, rather than from +# the live API. We hit the API endpoints at indexing time and store them as +# Solr documents, effectively precomputing all of the bits of data we need when +# displaying trees. + +require 'mixed_content_parser' + +class LargeTree + + include JSONModel + + WAYPOINT_SIZE = 200 + + def initialize(root_record, opts = {}) + @decorators = [] + + @root_record = root_record + + @root_table = @root_type = root_record.class.root_type.intern + @node_table = @node_type = root_record.class.node_type.intern + + @published_only = opts.fetch(:published_only, false) + end + + def add_decorator(decorator) + @decorators << decorator + end + + def published_filter + filter = {} + + filter[:publish] = @published_only ? [1] : [0, 1] + filter[:suppressed] = @published_only ? [0] : [0, 1] + + filter + end + + def root + DB.open do |db| + child_count = db[@node_table] + .filter(:root_record_id => @root_record.id, + :parent_id => nil) + .filter(published_filter) + .count + + response = waypoint_response(child_count).merge("title" => @root_record.title, + "uri" => @root_record.uri, + "jsonmodel_type" => @root_table.to_s) + @decorators.each do |decorator| + response = decorator.root(response, @root_record) + end + + precalculate_waypoints(response, nil) + + response + end + end + + def node(node_record) + DB.open do |db| + child_count = db[@node_table] + .filter(:root_record_id => @root_record.id, + :parent_id => node_record.id) + .filter(published_filter) + .count + + my_position = node_record.position + + node_position = db[@node_table] + .filter(:root_record_id => @root_record.id, + :parent_id => node_record.parent_id) + .filter(published_filter) + .where { position < my_position } + .count + 1 + + response = waypoint_response(child_count).merge("title" => node_record.display_string, + "uri" => node_record.uri, + "position" => node_position, + "jsonmodel_type" => @node_table.to_s) + + @decorators.each do |decorator| + response = decorator.node(response, node_record) + end + + precalculate_waypoints(response, node_record.id) + + response + end + end + + def node_from_root(node_ids, repo_id) + child_to_parent_map = {} + node_to_position_map = {} + node_to_root_record_map = {} + node_to_title_map = {} + + result = {} + + DB.open do |db| + ## Fetch our mappings of nodes to parents and nodes to positions + nodes_to_expand = node_ids + + while !nodes_to_expand.empty? + # Get the set of parents of the current level of nodes + next_nodes_to_expand = [] + + db[@node_table] + .filter(:id => nodes_to_expand) + .filter(published_filter) + .select(:id, :parent_id, :root_record_id, :position, :display_string).each do |row| + child_to_parent_map[row[:id]] = row[:parent_id] + node_to_position_map[row[:id]] = row[:position] + node_to_title_map[row[:id]] = row[:display_string] + node_to_root_record_map[row[:id]] = row[:root_record_id] + next_nodes_to_expand << row[:parent_id] + end + + nodes_to_expand = next_nodes_to_expand.compact.uniq + end + + ## Calculate the waypoint that each node will fall into + node_to_waypoint_map = {} + + (child_to_parent_map.keys + child_to_parent_map.values).compact.uniq.each do |node_id| + this_position = db[@node_type] + .filter(:parent_id => child_to_parent_map[node_id]) + .filter(:root_record_id => node_to_root_record_map[node_id]) + .filter(published_filter) + .where { position <= node_to_position_map[node_id] } + .count + + node_to_waypoint_map[node_id] = (this_position / WAYPOINT_SIZE) + end + + root_record_titles = {} + db[@root_table] + .join(@node_table, :root_record_id => :id) + .filter(Sequel.qualify(@node_table, :id) => node_ids) + .select(Sequel.qualify(@root_table, :id), + Sequel.qualify(@root_table, :title)) + .distinct + .each do |row| + root_record_titles[row[:id]] = row[:title] + end + + ## Build up the path of waypoints for each node + node_ids.each do |node_id| + root_record_id = node_to_root_record_map.fetch(node_id) + root_record_uri = JSONModel(@root_type).uri_for(root_record_id, :repo_id => repo_id) + + path = [] + + current_node = node_id + while child_to_parent_map[current_node] + parent_node = child_to_parent_map[current_node] + + path << {"node" => JSONModel(@node_type).uri_for(parent_node, :repo_id => repo_id), + "root_record_uri" => root_record_uri, + "title" => node_to_title_map.fetch(parent_node), + "offset" => node_to_waypoint_map.fetch(current_node), + "parsed_title" => MixedContentParser.parse(node_to_title_map.fetch(parent_node), '/')} + + current_node = parent_node + end + + path << {"node" => nil, + "root_record_uri" => root_record_uri, + "offset" => node_to_waypoint_map.fetch(current_node), + "title" => root_record_titles[root_record_id], + "parsed_title" => MixedContentParser.parse(root_record_titles[root_record_id], '/')} + + result[node_id] = path.reverse + end + end + + result + end + + + def waypoint(parent_id, offset) + record_ids = [] + records = {} + + DB.open do |db| + db[@node_table] + .filter(:root_record_id => @root_record.id, + :parent_id => parent_id) + .filter(published_filter) + .order(:position) + .select(:id, :repo_id, :title, :position) + .offset(offset * WAYPOINT_SIZE) + .limit(WAYPOINT_SIZE) + .each do |row| + record_ids << row[:id] + records[row[:id]] = row + end + + # Count up their children + child_counts = Hash[db[@node_table] + .filter(:root_record_id => @root_record.id, + :parent_id => records.keys) + .filter(published_filter) + .group_and_count(:parent_id) + .map {|row| [row[:parent_id], row[:count]]}] + + response = record_ids.each_with_index.map do |id, idx| + row = records[id] + child_count = child_counts.fetch(id, 0) + + waypoint_response(child_count).merge("title" => row[:title], + "parsed_title" => MixedContentParser.parse(row[:title], '/'), + "uri" => JSONModel(@node_type).uri_for(row[:id], :repo_id => row[:repo_id]), + "position" => (offset * WAYPOINT_SIZE) + idx, + "parent_id" => parent_id, + "jsonmodel_type" => @node_type.to_s) + + end + + @decorators.each do |decorator| + response = decorator.waypoint(response, record_ids) + end + + response + end + end + + private + + # When we return a list of waypoints, the client will pretty much always + # immediate ask us for the first one in the list. So, let's have the option + # of sending them back in the initial response for a given node to save it the + # extra request. + def precalculate_waypoints(response, parent_id) + response['precomputed_waypoints'] = {} + + uri = parent_id ? response['uri'] : "" + + if response['waypoints'] > 0 + response['precomputed_waypoints'][uri] ||= {} + response['precomputed_waypoints'][uri][0] = waypoint(parent_id, 0) + end + + response + end + + def waypoint_response(child_count) + { + "child_count" => child_count, + "waypoints" => (child_count.to_f / WAYPOINT_SIZE).ceil.to_i, + "waypoint_size" => WAYPOINT_SIZE, + } + end + +end diff --git a/backend/app/model/large_tree_classification.rb b/backend/app/model/large_tree_classification.rb new file mode 100644 index 0000000000..729e952ec3 --- /dev/null +++ b/backend/app/model/large_tree_classification.rb @@ -0,0 +1,15 @@ +class LargeTreeClassification + + def root(response, root_record) + response + end + + def node(response, node_record) + response + end + + def waypoint(response, record_ids) + response + end + +end diff --git a/backend/app/model/large_tree_digital_object.rb b/backend/app/model/large_tree_digital_object.rb new file mode 100644 index 0000000000..fb440e4066 --- /dev/null +++ b/backend/app/model/large_tree_digital_object.rb @@ -0,0 +1,76 @@ +class LargeTreeDigitalObject + + def root(response, root_record) + response['digital_object_type'] = root_record.digital_object_type + response['file_uri_summary'] = root_record.file_version.map {|file_version| + file_version[:file_uri] + }.join(", ") + + response + end + + def node(response, node_record) + response + end + + def waypoint(response, record_ids) + file_uri_by_digital_object_component = {} + + DigitalObjectComponent + .filter(:digital_object_component__id => record_ids) + .where(Sequel.~(:digital_object_component__label => nil)) + .select(Sequel.as(:digital_object_component__id, :id), + Sequel.as(:digital_object_component__label, :label)) + .each do |row| + id = row[:id] + result_for_record = response.fetch(record_ids.index(id)) + + result_for_record['label'] = row[:label] + end + + ASDate + .left_join(Sequel.as(:enumeration_value, :date_type), :id => :date__date_type_id) + .left_join(Sequel.as(:enumeration_value, :date_label), :id => :date__label_id) + .filter(:digital_object_component_id => record_ids) + .select(:digital_object_component_id, + Sequel.as(:date_type__value, :type), + Sequel.as(:date_label__value, :label), + :expression, + :begin, + :end) + .each do |row| + + id = row[:digital_object_component_id] + + result_for_record = response.fetch(record_ids.index(id)) + result_for_record['dates'] ||= [] + + date_data = {} + date_data['type'] = row[:type] if row[:type] + date_data['label'] = row[:label] if row[:label] + date_data['expression'] = row[:expression] if row[:expression] + date_data['begin'] = row[:begin] if row[:begin] + date_data['end'] = row[:end] if row[:end] + + result_for_record['dates'] << date_data + end + + FileVersion.filter(:digital_object_component_id => record_ids) + .select(:digital_object_component_id, + :file_uri) + .each do |row| + id = row[:digital_object_component_id] + + file_uri_by_digital_object_component[id] ||= [] + file_uri_by_digital_object_component[id] << row[:file_uri] + end + + file_uri_by_digital_object_component.each do |id, file_uris| + result_for_record = response.fetch(record_ids.index(id)) + result_for_record['file_uri_summary'] = file_uris.compact.join(", ") + end + + response + end + +end diff --git a/backend/app/model/large_tree_resource.rb b/backend/app/model/large_tree_resource.rb new file mode 100644 index 0000000000..2c753d2eae --- /dev/null +++ b/backend/app/model/large_tree_resource.rb @@ -0,0 +1,130 @@ +class LargeTreeResource + + def root(response, root_record) + response['level'] = root_record.other_level || root_record.level + + # Collect all container data + Instance + .join(:sub_container, :sub_container__instance_id => :instance__id) + .join(:top_container_link_rlshp, :sub_container_id => :sub_container__id) + .join(:top_container, :id => :top_container_link_rlshp__top_container_id) + .left_join(Sequel.as(:enumeration_value, :top_container_type), :id => :top_container__type_id) + .left_join(Sequel.as(:enumeration_value, :type_2), :id => :sub_container__type_2_id) + .left_join(Sequel.as(:enumeration_value, :type_3), :id => :sub_container__type_3_id) + .left_join(Sequel.as(:enumeration_value, :instance_type), :id => :instance__instance_type_id) + .filter(:resource_id => root_record.id) + .select(Sequel.as(:instance_type__value, :instance_type), + Sequel.as(:top_container_type__value, :top_container_type), + Sequel.as(:top_container__indicator, :top_container_indicator), + Sequel.as(:top_container__barcode, :top_container_barcode), + Sequel.as(:type_2__value, :type_2), + Sequel.as(:sub_container__indicator_2, :indicator_2), + Sequel.as(:type_3__value, :type_3), + Sequel.as(:sub_container__indicator_3, :indicator_3)) + .each do |row| + response['containers'] ||= [] + + container_data = {} + container_data['instance_type'] = row[:instance_type] if row[:instance_type] + container_data['top_container_type'] = row[:top_container_type] if row[:top_container_type] + container_data['top_container_indicator'] = row[:top_container_indicator] if row[:top_container_indicator] + container_data['top_container_barcode'] = row[:top_container_barcode] if row[:top_container_barcode] + container_data['type_2'] = row[:type_2] if row[:type_2] + container_data['indicator_2'] = row[:indicator_2] if row[:indicator_2] + container_data['type_3'] = row[:type_3] if row[:type_3] + container_data['indicator_3'] = row[:indicator_3] if row[:indicator_3] + + response['containers'] << container_data + end + + response + end + + def node(response, node_record) + response + end + + def waypoint(response, record_ids) + # Load the instance type and record level + ArchivalObject + .left_join(Sequel.as(:enumeration_value, :level_enum), :id => :archival_object__level_id) + .filter(:archival_object__id => record_ids) + .select(Sequel.as(:archival_object__id, :id), + Sequel.as(:level_enum__value, :level), + Sequel.as(:archival_object__other_level, :other_level)) + .each do |row| + id = row[:id] + result_for_record = response.fetch(record_ids.index(id)) + + result_for_record['level'] = row[:other_level] || row[:level] + end + + ASDate + .left_join(Sequel.as(:enumeration_value, :date_type), :id => :date__date_type_id) + .left_join(Sequel.as(:enumeration_value, :date_label), :id => :date__label_id) + .filter(:archival_object_id => record_ids) + .select(:archival_object_id, + Sequel.as(:date_type__value, :type), + Sequel.as(:date_label__value, :label), + :expression, + :begin, + :end) + .each do |row| + + id = row[:archival_object_id] + + result_for_record = response.fetch(record_ids.index(id)) + result_for_record['dates'] ||= [] + + date_data = {} + date_data['type'] = row[:type] if row[:type] + date_data['label'] = row[:label] if row[:label] + date_data['expression'] = row[:expression] if row[:expression] + date_data['begin'] = row[:begin] if row[:begin] + date_data['end'] = row[:end] if row[:end] + + result_for_record['dates'] << date_data + end + + # Display container information + Instance + .join(:sub_container, :sub_container__instance_id => :instance__id) + .join(:top_container_link_rlshp, :sub_container_id => :sub_container__id) + .join(:top_container, :id => :top_container_link_rlshp__top_container_id) + .left_join(Sequel.as(:enumeration_value, :top_container_type), :id => :top_container__type_id) + .left_join(Sequel.as(:enumeration_value, :type_2), :id => :sub_container__type_2_id) + .left_join(Sequel.as(:enumeration_value, :type_3), :id => :sub_container__type_3_id) + .left_join(Sequel.as(:enumeration_value, :instance_type), :id => :instance__instance_type_id) + .filter(:archival_object_id => record_ids) + .select(:archival_object_id, + Sequel.as(:instance_type__value, :instance_type), + Sequel.as(:top_container_type__value, :top_container_type), + Sequel.as(:top_container__indicator, :top_container_indicator), + Sequel.as(:top_container__barcode, :top_container_barcode), + Sequel.as(:type_2__value, :type_2), + Sequel.as(:sub_container__indicator_2, :indicator_2), + Sequel.as(:type_3__value, :type_3), + Sequel.as(:sub_container__indicator_3, :indicator_3)) + .each do |row| + id = row[:archival_object_id] + + result_for_record = response.fetch(record_ids.index(id)) + result_for_record['containers'] ||= [] + + container_data = {} + container_data['instance_type'] = row[:instance_type] if row[:instance_type] + container_data['top_container_type'] = row[:top_container_type] if row[:top_container_type] + container_data['top_container_indicator'] = row[:top_container_indicator] if row[:top_container_indicator] + container_data['top_container_barcode'] = row[:top_container_barcode] if row[:top_container_barcode] + container_data['type_2'] = row[:type_2] if row[:type_2] + container_data['indicator_2'] = row[:indicator_2] if row[:indicator_2] + container_data['type_3'] = row[:type_3] if row[:type_3] + container_data['indicator_3'] = row[:indicator_3] if row[:indicator_3] + + result_for_record['containers'] << container_data + end + + response + end + +end diff --git a/backend/app/model/mixins/agent_manager.rb b/backend/app/model/mixins/agent_manager.rb index 0dc6d6eaa2..89f3669961 100644 --- a/backend/app/model/mixins/agent_manager.rb +++ b/backend/app/model/mixins/agent_manager.rb @@ -321,6 +321,14 @@ def my_agent_type def sequel_to_jsonmodel(objs, opts = {}) jsons = super + if opts[:calculate_linked_repositories] + agents_to_repositories = GlobalRecordRepositoryLinkages.new(self, :linked_agents).call(objs) + + jsons.zip(objs).each do |json, obj| + json.used_within_repositories = agents_to_repositories.fetch(obj, []).map {|repo| repo.uri} + end + end + jsons.zip(objs).each do |json, obj| json.agent_type = my_agent_type[:jsonmodel].to_s json.linked_agent_roles = obj.linked_agent_roles diff --git a/backend/app/model/mixins/archival_object_series.rb b/backend/app/model/mixins/archival_object_series.rb deleted file mode 100644 index 9b058c78c3..0000000000 --- a/backend/app/model/mixins/archival_object_series.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ArchivalObjectSeries - - def topmost_archival_object - if self.parent_id - self.class[self.parent_id].topmost_archival_object - else - self - end - end - - - def series - top_ao = topmost_archival_object - - if top_ao.has_series_specific_fields? - top_ao - else - nil - end - end - - - def has_series_specific_fields? - component_id && (level == "series" || (level == "otherlevel" && !other_level.nil? && other_level.downcase == "accession")) - end - -end diff --git a/backend/app/model/mixins/reindex_top_containers.rb b/backend/app/model/mixins/reindex_top_containers.rb index f160d98b07..c5851d9c69 100644 --- a/backend/app/model/mixins/reindex_top_containers.rb +++ b/backend/app/model/mixins/reindex_top_containers.rb @@ -9,7 +9,7 @@ def reindex_top_containers(extra_ids = []) # Find any relationships between a top container and any instance within the current tree. root_record = if self.class == ArchivalObject - self.root_record_id ? self.class.root_model[self.root_record_id] : self.topmost_archival_object + self.class.root_model[self.root_record_id] else self end @@ -40,7 +40,7 @@ def reindex_top_containers(extra_ids = []) def reindex_top_containers_by_any_means_necessary(extra_ids) # Find any relationships between a top container and any instance within the current tree. root_record = if self.class == ArchivalObject - self.root_record_id ? self.class.root_model[self.root_record_id] : self.topmost_archival_object + self.class.root_model[self.root_record_id] else self end @@ -58,11 +58,15 @@ def reindex_top_containers_by_any_means_necessary(extra_ids) # not defined in accession or resource - def update_position_only(*) + def set_parent_and_position(*) super reindex_top_containers end + def set_root(*) + super + reindex_top_containers + end def delete reindex_top_containers diff --git a/backend/app/model/mixins/relationships.rb b/backend/app/model/mixins/relationships.rb index b8b76b9376..60684877a1 100644 --- a/backend/app/model/mixins/relationships.rb +++ b/backend/app/model/mixins/relationships.rb @@ -713,53 +713,111 @@ def touch_mtime_of_anyone_related_to(obj) relationships.map do |relationship_defn| models = relationship_defn.participating_models - if models.include?(obj.class) - their_ref_columns = relationship_defn.reference_columns_for(obj.class) - my_ref_columns = relationship_defn.reference_columns_for(self) - their_ref_columns.each do |their_col| - my_ref_columns.each do |my_col| - - # Example: if we're updating a subject record and want to update - # the timestamps of any linked archival object records: - # - # * self = ArchivalObject - # * relationship_defn is subject_rlshp - # * obj = # - # * their_col = subject_rlshp.subject_id - # * my_col = subject_rlshp.archival_object_id - - if DB.supports_join_updates? - - if self.table_name == :agent_software && relationship_defn.table_name == :linked_agents_rlshp - # Terrible to have to do this, but the MySQL optimizer refuses - # to use the primary key on agent_software because it (often) - # only has one row. - DB.open do |db| - id_str = Integer(obj.id).to_s - - db.run("UPDATE `agent_software` FORCE INDEX (PRIMARY) " + - " INNER JOIN `linked_agents_rlshp` " + - "ON (`linked_agents_rlshp`.`agent_software_id` = `agent_software`.`id`) " + - "SET `agent_software`.`system_mtime` = NOW() " + - "WHERE (`linked_agents_rlshp`.`archival_object_id` = #{id_str})") - end - else - # MySQL will optimize this much more aggressively - self.join(relationship_defn, Sequel.qualify(relationship_defn.table_name, my_col) => Sequel.qualify(self.table_name, :id)). - filter(Sequel.qualify(relationship_defn.table_name, their_col) => obj.id). - update(Sequel.qualify(self.table_name, :system_mtime) => now) - end - - else - ids_to_touch = relationship_defn.filter(their_col => obj.id). - select(my_col) - self.filter(:id => ids_to_touch). - update(:system_mtime => now) + # If this relationship doesn't link to records of type `obj`, we're not + # interested. + next unless models.include?(obj.class) + + their_ref_columns = relationship_defn.reference_columns_for(obj.class) + my_ref_columns = relationship_defn.reference_columns_for(self) + their_ref_columns.each do |their_col| + my_ref_columns.each do |my_col| + + # This one type of relationship (between the software agent and + # anything else) was a particular hotspot when analyzing real-world + # performance. + # + # Terrible to have to do this, but the MySQL optimizer refuses + # to use the primary key on agent_software because it (often) + # only has one row. + # + if DB.supports_join_updates? && + self.table_name == :agent_software && + relationship_defn.table_name == :linked_agents_rlshp + DB.open do |db| + id_str = Integer(obj.id).to_s + + db.run("UPDATE `agent_software` FORCE INDEX (PRIMARY) " + + " INNER JOIN `linked_agents_rlshp` " + + "ON (`linked_agents_rlshp`.`agent_software_id` = `agent_software`.`id`) " + + "SET `agent_software`.`system_mtime` = NOW() " + + "WHERE (`linked_agents_rlshp`.`archival_object_id` = #{id_str})") end + + return end + + # Example: if we're updating a subject record and want to update + # the timestamps of any linked archival object records: + # + # * self = ArchivalObject + # * relationship_defn is subject_rlshp + # * obj = # + # * their_col = subject_rlshp.subject_id + # * my_col = subject_rlshp.archival_object_id + + + # Join our model class table to the relationship that links it to `obj` + # + # For example: join ArchivalObject to subject_rlshp + # join Instance to instance_do_link_rlshp + base_ds = self.join(relationship_defn, + Sequel.qualify(relationship_defn.table_name, my_col) => + Sequel.qualify(self.table_name, :id)) + + # Limit only to the object of interest--we only care about records + # involved in a relationship with the record that was updated (obj) + base_ds = base_ds.filter(Sequel.qualify(relationship_defn.table_name, their_col) => obj.id) + + # Now update the mtime of any top-level record that links to that + # relationship. + self.update_toplevel_mtimes(base_ds, now) end end end end + + # Given a `dataset` that links the current record type to some relationship + # type, set the modification time of the nearest top-level record to + # `new_mtime`. + # + # If the current record type links directly to the relationship (such as an + # Archival Object linking to a Subject), then this is easy: we just update + # the modification time of the Archival Object. + # + # If the current record is a nested record (such as an Instance linked to a + # Digital Object), we want to continue up the chain, linking the Instance + # nested record to its Accession/Resource/Archival Object parent record, and + # then update the modification time of that parent. + # + # And if the nested record has a nested record has a nested record has a + # relationship... well, you get the idea. We handle the recursive case too! + # + def update_toplevel_mtimes(dataset, new_mtime) + if self.enclosing_associations.empty? + # If we're not enclosed by anything else, we're a top-level record. Do the final update. + if DB.supports_join_updates? + # Fast path! Use a join update. + dataset.update(Sequel.qualify(self.table_name, :system_mtime) => new_mtime) + else + # Slow path. Subselect. + ids_to_touch = dataset.select(Sequel.qualify(self.table_name, :id)) + self.filter(:id => ids_to_touch).update(:system_mtime => new_mtime) + end + else + # Otherwise, we're a nested record + self.enclosing_associations.each do |association| + parent_model = association[:model] + + # Link the parent into the current dataset + parent_ds = dataset.join(parent_model, + Sequel.qualify(self.table_name, association[:key]) => + Sequel.qualify(parent_model.table_name, :id)) + + # and tell it to continue! + parent_model.update_toplevel_mtimes(parent_ds, new_mtime) + end + end + end + end end diff --git a/backend/app/model/mixins/restriction_calculator.rb b/backend/app/model/mixins/restriction_calculator.rb index c809c8fe3e..61b2455821 100644 --- a/backend/app/model/mixins/restriction_calculator.rb +++ b/backend/app/model/mixins/restriction_calculator.rb @@ -87,7 +87,7 @@ def self.expand_to_tree(model, id_set) { model => rec_ids, - model.root_model => model.filter(:id => rec_ids).select(:root_record_id).map(&:root_record_id).uniq + model.root_model => model.filter(:id => rec_ids).select(:root_record_id).distinct.map(&:root_record_id) } end diff --git a/backend/app/model/mixins/tree_nodes.rb b/backend/app/model/mixins/tree_nodes.rb index c7c3b697c6..237361ccd2 100644 --- a/backend/app/model/mixins/tree_nodes.rb +++ b/backend/app/model/mixins/tree_nodes.rb @@ -1,141 +1,179 @@ # Mixin methods for objects that belong in an ordered hierarchy (archival # objects, digital object components) -require 'securerandom' module TreeNodes + # We'll space out our positions by this amount. This means we can insert + # log2(POSITION_STEP) nodes before any given node before needing to rebalance. + # + # Sized according to the largest number of nodes we think we might see under a + # single parent. The size of the position column is 2^31, so position can be + # anywhere up to about 2 billion. For a step size of 1000, that means we can + # support (/ (expt 2 31) 1000) positions (around 2 million) before running out + # of numbers. + # + POSITION_STEP = 1000 + + # The number of times we'll retry an update that might transiently fail due to + # concurrent updates. + DB_RETRIES = 100 + def self.included(base) base.extend(ClassMethods) end - + # Move this node (and all records under it) to a new tree. def set_root(new_root) self.root_record_id = new_root.id - save - refresh if self.parent_id.nil? - # Set ourselves to the end of the list - update_position_only(nil, nil) - else - update_position_only(self.parent_id, self.position) + # This top-level node has been moved to a new tree. Append it to the end of the list. + root_uri = self.class.uri_for(self.class.root_record_type.intern, self.root_record_id) + self.parent_name = "root@#{root_uri}" + + self.position = self.class.next_position_for_parent(self.parent_name) end + save + refresh + children.each do |child| child.set_root(new_root) end end - - - def siblings - self.class.dataset. - filter(:root_record_id => self.root_record_id, - :parent_id => self.parent_id, - ~:position => nil) - end - # this is just a CYA method, that might be removed in the future. We need to - # be sure that all the positional gaps.j - def order_siblings - # add this to avoid DB constraints - siblings.update(:parent_name => Sequel.lit(DB.concat('CAST(id as CHAR(10))', "'_temp'"))) - - # get set a list of ids and their order based on their position - position_map = siblings.select(:id).order(:position).each_with_index.inject({}) { |m,( obj, index) | m[obj[:id]] = index; m } - - # now we do the update in batches of 200 - position_map.each_slice(200) do |pm| - # the slice reformat the hash...so quickly format it back - pm = pm.inject({}) { |m,v| m[v.first] = v.last; m } - # this ids that we're updating in this batch - sibling_ids = pm.keys - - # the resulting update will look like: - # UPDATE "ARCHIVAL_OBJECT" SET "POSITION" = (CASE WHEN ("ID" = 10914) - # THEN 0 WHEN ("ID" = 10915) THEN 1 WHEN ("ID" = 10912) THEN 2 WHEN - # ("ID" = 10913) THEN 3 WHEN ("ID" = 10916) THEN 4 WHEN ("ID" = 10921) - # THEN 5 WHEN ("ID" = 10917) THEN 6 WHEN ("ID" = 10920) THEN 7 ELSE 0 - # END) WHERE (("ROOT_RECORD_ID" = 3) AND ("PARENT_ID" = 10911) AND (NOT - # "POSITION" IS NULL) AND ("ID" IN (10914, 10915, 10912, 10913, 10916, - # 10921, 10917, 10920)) - # ) - # this should be faster than just iterating thru all the children, - # since it does it in batches of 200 and limits the number of updates. - siblings.filter(:id => sibling_ids).update( :position => Sequel.case(pm, 0, :id) ) + def set_position_in_list(target_logical_position) + self.class.retry_db_update do + attempt_set_position_in_list(target_logical_position) end - - # now we return the parent_name back so our DB constraints are back on.:w - siblings.update(:parent_name => self.parent_name ) end - def set_position_in_list(target_position, sequence) - - # Find the position of the element we'll be inserted after. If there are no - # elements, or if our target position is zero, then we'll get inserted at - # position zero. - predecessor = if target_position > 0 - siblings.filter(~:id => self.id).order(:position).limit(target_position).select(:position).all - else - [] - end - new_position = !predecessor.empty? ? (predecessor.last[:position] + 1) : 0 + # A note on terminology: a logical position refers to the position of a node + # as observed by the user (0...RECORD_COUNT). A physical position is the + # position number stored in the database, which may have gaps. + def attempt_set_position_in_list(target_logical_position) + DB.open do |db| + ordered_siblings = db[self.class.node_model.table_name].filter(:parent_name => self.parent_name).order(:position) + siblings_count = ordered_siblings.count + target_logical_position = [target_logical_position, siblings_count - 1].min - 100.times do - DB.attempt { - # Go right to the database here to avoid bumping lock_version for tree changes. - self.class.dataset.db[self.class.table_name].filter(:id => self.id).update(:position => new_position) + current_physical_position = self.position + current_logical_position = ordered_siblings.where { position < current_physical_position }.count + + # We'll determine which node will fall to the left of our moved node, and + # which will fall to the right. We're going to set our physical position to + # the halfway point of those two nodes. For example, if left node is + # position 1000 and right node is position 2000, we'll take position 1500. + # If there's no gap, we'll create one! + # + left_node_idx = target_logical_position - 1 - return - }.and_if_constraint_fails { - # Someone's in our spot! Move everyone out of the way and retry. + if current_logical_position < target_logical_position + # If the node is being moved to the right, we need to adjust our index to + # compensate for the fact that everything logically shifts to the left as we + # pop it out. + left_node_idx += 1 + end - # Bump the sequence to maintain the invariant that sequence.number >= max(position) - # (since we're about to increment the last N positions by 1) - Sequence.get(sequence) + left_node_physical_position = + if left_node_idx < 0 + # We'll be the first item in the list (nobody to the left of us) + nil + else + ordered_siblings.offset(left_node_idx).get(:position) + end - # Sigh. Work around: - # http://stackoverflow.com/questions/5403437/atomic-multi-row-update-with-a-unique-constraint - siblings. - filter { position >= new_position }. - update(:parent_name => Sequel.lit(DB.concat('CAST(id as CHAR(10))', "'_temp'"))) + right_node_idx = left_node_idx + 1 - # Do the update we actually wanted - siblings. - filter { position >= new_position }. - update(:position => Sequel.lit('position + 1')) + right_node_physical_position = + if right_node_idx >= siblings_count + # We'll be the last item in the list (nobody to the right of us) + nil + else + ordered_siblings.offset(right_node_idx).get(:position) + end + new_position = + if left_node_physical_position.nil? && right_node_physical_position.nil? + # We're first in the list! + new_position = TreeNodes::POSITION_STEP + else + if right_node_physical_position.nil? + # Add to the end + left_node_physical_position + TreeNodes::POSITION_STEP + else + left_node_physical_position ||= 0 + + if (right_node_physical_position - left_node_physical_position) <= 1 + # We need to create a gap to fit our moved node + right_node_physical_position = ensure_gap(right_node_physical_position) + end + + # Put the node we're moving halfway between the left and right nodes + left_node_physical_position + ((right_node_physical_position - left_node_physical_position) / 2) + end + end - # Puts it back again - siblings. - filter { position >= new_position}. - update(:parent_name => self.parent_name ) - # Now there's a gap at new_position ready for our element. - } + self.class.dataset.db[self.class.table_name] + .filter(:id => self.id) + .update(:position => new_position, + :system_mtime => Time.now) end + end + + def ensure_gap(start_physical_position) + siblings = self.class.dataset + .filter(:root_record_id => self.root_record_id) + .filter(:parent_id => self.parent_id) + .filter { position >= start_physical_position } - raise "Failed to set the position for #{self}" + # Sigh. Work around: + # http://stackoverflow.com/questions/5403437/atomic-multi-row-update-with-a-unique-constraint + siblings.update(:parent_name => Sequel.lit(DB.concat('CAST(id as CHAR(10))', "'_temp'"))) + + # Do the real update + siblings.update(:position => Sequel.lit('position + ' + TreeNodes::POSITION_STEP.to_s), + :system_mtime => Time.now) + + # Puts it back again + siblings.update(:parent_name => self.parent_name) + + start_physical_position + TreeNodes::POSITION_STEP end - def absolute_position + + def logical_position relative_position = self.position self.class.dataset.filter(:parent_name => self.parent_name).where { position < relative_position }.count end - def update_from_json(json, opts = {}, apply_nested_records = true) - sequence = self.class.sequence_for(json) + def update_from_json(json, extra_values = {}, apply_nested_records = true) + root_uri = self.class.uri_for(self.class.root_record_type, self.root_record_id) - self.class.set_root_record(json, sequence, opts) + do_position_override = json[self.class.root_record_type]['ref'] != root_uri || extra_values[:force_reposition] - obj = super + if do_position_override + extra_values.delete(:force_reposition) + json.position = nil + # Through some inexplicable sequence of events, the update is allowed to + # change the root record on the fly. I guess we'll allow this... + extra_values = extra_values.merge(self.class.determine_tree_position_for_new_node(json)) + else + if !json.position + # The incoming JSON had no position set. Just keep what we already had. + extra_values['position'] = self.position + end + end + + obj = super(json, extra_values, apply_nested_records) - # Then lock in a position (which may involve contention with other updates - # happening to the same tree of records) - if json[self.class.root_record_type] && json.position - self.set_position_in_list(json.position, sequence) + if json.position + # Our incoming JSON wants to set the position. That's fine + set_position_in_list(json.position) end trigger_index_of_child_nodes @@ -150,50 +188,52 @@ def trigger_index_of_child_nodes end - def update_position_only(parent_id, position) - if self[:root_record_id] - root_uri = self.class.uri_for(self.class.root_record_type.intern, self[:root_record_id]) - parent_uri = parent_id ? self.class.uri_for(self.class.node_record_type.intern, parent_id) : root_uri - sequence = "#{parent_uri}_children_position" - - parent_name = if parent_id - "#{parent_id}@#{self.class.node_record_type}" - else - "root@#{root_uri}" - end - - - new_values = { - :parent_id => parent_id, - :parent_name => parent_name, - :position => Sequence.get(sequence), - :system_mtime => Time.now - } - - # Run through the standard validation without actually saving - self.set(new_values) - self.validate - - if self.errors && !self.errors.empty? - raise Sequel::ValidationFailed.new(self.errors) - end - - - # let's try and update the position. If it doesn't work, then we'll fix - # the position when we set it in the list...there can be problems when - # transfering to another repo when there's holes in the tree... - DB.attempt { - self.class.dataset.filter(:id => self.id).update(new_values) - }.and_if_constraint_fails { - new_values.delete(:position) - self.class.dataset.filter(:id => self.id).update(new_values) - } - - self.refresh - self.set_position_in_list(position, sequence) if position + def set_parent_and_position(parent_id, position) + self.class.retry_db_update do + attempt_set_parent_and_position(parent_id, position) + end + end + + + def attempt_set_parent_and_position(parent_id, position) + root_uri = self.class.uri_for(self.class.root_record_type.intern, self[:root_record_id]) + + if self.id == parent_id + raise "Can't make a record into its own parent" + end + + parent_name = if parent_id + "#{parent_id}@#{self.class.node_record_type}" + else + "root@#{root_uri}" + end + + new_values = { + :parent_id => parent_id, + :parent_name => parent_name, + :system_mtime => Time.now + } + + if parent_name == self.parent_name + # Position is unchanged initially + new_values[:position] = self.position else - raise "Root not set for record #{self.inspect}" + # Append this node to the new parent initially + new_values[:position] = self.class.next_position_for_parent(parent_name) end + + # Run through the standard validation without actually saving + self.set(new_values) + self.validate + + if self.errors && !self.errors.empty? + raise Sequel::ValidationFailed.new(self.errors) + end + + self.class.dataset.filter(:id => self.id).update(new_values) + self.refresh + + self.set_position_in_list(position) end @@ -208,26 +248,36 @@ def has_children? def transfer_to_repository(repository, transfer_group = []) - - # All records under this one will be transferred too children.each_with_index do |child, i| child.transfer_to_repository(repository, transfer_group + [self]) - # child.update_position_only( child.parent_id, i ) - end - - RequestContext.open(:repo_id => repository.id ) do - self.update_position_only(self.parent_id, self.position) unless self.root_record_id.nil? end - - # ensure that the sequence if updated - - super + + # ensure that the sequence if updated + super end module ClassMethods + def retry_db_update(&block) + finished = false + last_error = nil + + TreeNodes::DB_RETRIES.times do + break if finished + + DB.attempt { + block.call + return + }.and_if_constraint_fails {|err| + last_error = err + } + end + + raise last_error + end + def tree_record_types(root, node) @root_record_type = root.to_s @node_record_type = node.to_s @@ -251,36 +301,35 @@ def node_model Kernel.const_get(node_record_type.camelize) end + def create_from_json(json, extra_values = {}) + obj = nil - def sequence_for(json) - if json[root_record_type] - if json.parent - "#{json.parent['ref']}_children_position" - else - "#{json[root_record_type]['ref']}_children_position" + retry_db_update do + DB.open do + position_values = determine_tree_position_for_new_node(json) + obj = super(json, extra_values.merge(position_values)) end end - end - def create_from_json(json, opts = {}) - sequence = sequence_for(json) - set_root_record(json, sequence, opts) - - obj = super - - migration = opts[:migration] ? opts[:migration].value : false - if json[self.root_record_type] && json.position && !migration - obj.set_position_in_list(json.position, sequence) + if obj.nil? + Log.error("Failed to set the position for #{node_model}: #{last_error}") + raise last_error + end + + migration = extra_values[:migration] ? extra_values[:migration].value : false + if json.position && !migration + obj.set_position_in_list(json.position) end obj end - def set_root_record(json, sequence, opts) - opts["root_record_id"] = nil - opts["parent_id"] = nil - opts["parent_name"] = nil + def determine_tree_position_for_new_node(json) + result = {} + + root_record_uri = json[root_record_type]['ref'] + result["root_record_id"] = JSONModel.parse_reference(root_record_uri).fetch(:id) # 'parent_name' is a bit funny. We need this column because the combination # of (parent, position) needs to be unique, to ensure that two siblings @@ -291,26 +340,37 @@ def set_root_record(json, sequence, opts) # So, parent_name gets used as a stand in in this case: it always has a # value for any node belonging to a hierarchy, and this value gets used in # the uniqueness check. + # + if json.parent + parent_id = JSONModel.parse_reference(json.parent['ref']).fetch(:id) - if json[root_record_type] - opts["root_record_id"] = parse_reference(json[root_record_type]['ref'], opts)[:id] + result["parent_id"] = parent_id + result["parent_name"] = "#{parent_id}@#{self.node_record_type}" + else + result["parent_id"] = nil + result["parent_name"] = "root@#{root_record_uri}" + end - if json.parent - opts["parent_id"] = parse_reference(json.parent['ref'], opts)[:id] - opts["parent_name"] = "#{opts['parent_id']}@#{self.node_record_type}" - else - opts["parent_name"] = "root@#{json[root_record_type]['ref']}" - end + # We'll add this new node to the end of the list. To do that, find the + # maximum position assigned so far and go TreeNodes::POSITION_STEP places + # after that. If another create_from_json gets in first, we'll have to + # retry, but that's fine. + result["position"] = next_position_for_parent(result['parent_name']) - opts["position"] = Sequence.get(sequence) + result + end - else - # This record isn't part of a tree hierarchy - opts["parent_name"] = "orphan@#{SecureRandom.uuid}" - opts["position"] = 0 + def next_position_for_parent(parent_name) + max_position = DB.open do |db| + db[node_model.table_name] + .filter(:parent_name => parent_name) + .select(:position) + .max(:position) end - end + max_position ||= 0 + max_position + TreeNodes::POSITION_STEP + end def sequel_to_jsonmodel(objs, opts = {}) jsons = super @@ -324,13 +384,13 @@ def sequel_to_jsonmodel(objs, opts = {}) end if obj.parent_name - # Calculate the absolute (gapless) position of this node. This + # Calculate the logical (gapless) position of this node. This # bridges the gap between the DB's view of position, which only # cares that the positions order correctly, with the API's view, - # which speaks in absolute numbering (i.e. the first position is 0, + # which speaks in logical numbering (i.e. the first position is 0, # the second position is 1, etc.) - json.position = obj.absolute_position + json.position = obj.logical_position end end @@ -380,43 +440,17 @@ def calculate_object_graph(object_graph, opts = {}) def handle_delete(ids_to_delete) ids = self.filter(:id => ids_to_delete ) + + # Update the root record's mtime so that any tree-related records are reindexed + root_model.filter(:id => ids.select(:root_record_id)).update(:system_mtime => Time.now) + # lets get a group of records that have unique parents or root_records parents = ids.select_group(:parent_id, :root_record_id).all # we then nil out the parent id so deletes can do its thing ids.update(:parent_id => nil) # trigger the deletes... - obj = super - # now lets make sure there are no holes - parents.each do |parent| - children = self.filter(:root_record_id => parent[:root_record_id], :parent_id => parent[:parent_id], ~:position => nil ) - parent_name = children.get(:parent_name) - children.update(:parent_name => Sequel.lit(DB.concat('CAST(id as CHAR(10))', "'_temp'"))) - children.order(:position).each_with_index do |row, i| - row.update(:position => i) - end - children.update(:parent_name => parent_name) - end - obj - end - - # this requences the class, which updates the Sequence with correct - # sequences - def resequence(repo_id) - RequestContext.open(:repo_id => repo_id) do - # get all the objects that are parents but not at top level or orphans - $stderr.puts "Resequencing for #{self.class.to_s} in repo #{repo_id}" - self.filter(~:position => nil, :repo_id => repo_id, ~:parent_id => nil, ~:root_record_id => nil ).select(:parent_id).group(:parent_id) - .each do |obj| - $stderr.print "+" - self.filter(:parent_id => obj.parent_id).order(:position).each_with_index do |child, i| - $stderr.print "." - child.update_position_only(child.parent_id, i) - end - end - $stderr.puts "*" - end + super end - end end diff --git a/backend/app/model/mixins/trees.rb b/backend/app/model/mixins/trees.rb index 398669b9d6..a6c703de04 100644 --- a/backend/app/model/mixins/trees.rb +++ b/backend/app/model/mixins/trees.rb @@ -8,11 +8,12 @@ def self.included(base) def adopt_children(old_parent) - self.class.node_model. - this_repo.filter(:root_record_id => old_parent.id, - :parent_id => nil).order(:position).each do |root_child| - root_child.set_root(self) - end + self.class.node_model.this_repo + .filter(:root_record_id => old_parent.id, + :parent_id => nil) + .order(:position).each do |root_child| + root_child.set_root(self) + end end @@ -171,6 +172,44 @@ def tree(ids_of_interest = :all) JSONModel("#{self.class.root_type}_tree".intern).from_hash(result, true, true) end + # Return a depth-first-ordered list of URIs under this tree (starting with the tree itself) + def ordered_records + id_positions = {} + parent_to_child_id = {} + + self.class.node_model + .filter(:root_record_id => self.id) + .select(:id, :position, :parent_id).each do |row| + + id_positions[row[:id]] = row[:position] + parent_to_child_id[row[:parent_id]] ||= [] + parent_to_child_id[row[:parent_id]] << row[:id] + end + + result = [] + + # Start with top-level records + root_set = [nil] + id_positions[nil] = 0 + + while !root_set.empty? + next_rec = root_set.shift + if next_rec.nil? + # Our first iteration. Nothing to add yet. + else + result << next_rec + end + + children = parent_to_child_id.fetch(next_rec, []).sort_by {|child| id_positions[child]} + children.reverse.each do |child| + root_set.unshift(child) + end + end + + [{'ref' => self.uri}] + + result.map {|id| {'ref' => self.class.node_model.uri_for(self.class.node_type, id)} + } + end def transfer_to_repository(repository, transfer_group = []) obj = super diff --git a/backend/app/model/preference.rb b/backend/app/model/preference.rb index 816dd95680..7175065f35 100644 --- a/backend/app/model/preference.rb +++ b/backend/app/model/preference.rb @@ -7,7 +7,7 @@ class Preference < Sequel::Model(:preference) def self.init defs_file = File.join(ASUtils.find_base_directory("common"), "config", "preference_defaults.rb") defaults = {} - if File.exists?(defs_file) + if File.exist?(defs_file) found_defs_file = true Log.info("Loading preference defaults file at #{defs_file}") defaults = eval(File.read(defs_file)) diff --git a/backend/app/model/reports/abstract_report.rb b/backend/app/model/reports/abstract_report.rb index 4380a2dcc0..757d31ec79 100644 --- a/backend/app/model/reports/abstract_report.rb +++ b/backend/app/model/reports/abstract_report.rb @@ -6,36 +6,52 @@ class AbstractReport attr_accessor :repo_id attr_accessor :format attr_accessor :params + attr_accessor :db + attr_accessor :orientation + attr_reader :job - def initialize(params, job) + def initialize(params, job, db) # sanity check, please. params = params.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo} @repo_id = params[:repo_id] if params.has_key?(:repo_id) && params[:repo_id] != "" @format = params[:format] if params.has_key?(:format) && params[:format] != "" @params = params @job = job + @db = db end - def get_binding - binding + def title + I18n.t("reports.#{code}.title", :default => code) end - def title - self.class.name + def new_subreport(subreport_model, params) + subreport_model.new(params.merge(:format => 'html'), job, db) + end + + def get_binding + binding end def report self end + def headers + query.columns.map(&:to_s) + end + def template - :'reports/_listing' + 'generic_listing.erb' end def layout AppConfig[:report_page_layout] end + def orientation + "portrait" + end + def processor {} end @@ -44,24 +60,27 @@ def current_user @job.owner end - def query(db) + def query(db = @db) raise "Please specify a query to return your reportable results" end - def scope_by_repo_id(dataset) - dataset.where(:repo_id => @repo_id) - end + def each(db = @db) + dataset = query + dataset.where(:repo_id => @repo_id) if @repo_id - def each - DB.open do |db| - dataset = query(db) - dataset = scope_by_repo_id(dataset) if @repo_id - dataset.each do |row| - yield(Hash[headers.map { |h| - val = (processor.has_key?(h))?processor[h].call(row):row[h.intern] - [h, val] - }]) - end + dataset.each do |row| + yield(Hash[(headers + processor.keys).uniq.map { |h| + val = (processor.has_key?(h))?processor[h].call(row):row[h.intern] + [h, val] + }]) end end + + def code + self.class.code + end + + def self.code + self.name.gsub(/(.)([A-Z])/,'\1_\2').downcase + end end diff --git a/backend/app/model/reports/jasper_report.rb b/backend/app/model/reports/jasper_report.rb deleted file mode 100644 index b3a7a95054..0000000000 --- a/backend/app/model/reports/jasper_report.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'java' -require_relative 'report_manager' - -require_relative '../../lib/static_asset_finder' - -require 'tempfile' -require 'rjack-jackson' - -java_import java.util.Locale - -java_import Java::net::sf::jasperreports::engine::JRException -java_import Java::net::sf::jasperreports::engine::JRParameter -java_import Java::net::sf::jasperreports::engine::JasperExportManager -java_import Java::net::sf::jasperreports::engine::JasperFillManager -java_import Java::net::sf::jasperreports::engine::JasperCompileManager -java_import Java::net::sf::jasperreports::engine::util::JRLoader -java_import Java::net::sf::jasperreports::export::SimpleExporterInput -java_import Java::net::sf::jasperreports::export::SimpleWriterExporterOutput -java_import Java::net::sf::jasperreports::export::SimpleOutputStreamExporterOutput -java_import Java::net::sf::jasperreports::export::SimpleXlsxReportConfiguration -java_import Java::net::sf::jasperreports::engine::export::JRCsvExporter -java_import Java::net::sf::jasperreports::engine::export::ooxml::JRXlsxExporter -java_import Java::net::sf::jasperreports::engine::query::JsonQueryExecuterFactory -java_import Java::org::apache::commons::lang::StringUtils - -class JasperReport - - include ReportManager::Mixin - include Java - - attr_accessor :jrprint - attr_accessor :export_file - attr_accessor :data_source - attr_accessor :format - - def initialize(params, job) - @repo_id = params[:repo_id] if params.has_key?(:repo_id) && params[:repo_id] != "" - @base_path = File.dirname(self.class.report) - @format = params[:format] if params.has_key?(:format) && params[:format] != "" - ObjectSpace.define_finalizer( self, self.class.finalize(self) ) - end - - def title - self.class.name - end - - # the convention is that all report files ( primary and subreports) will be located in - # AS_BASE/reports/ClassNameReport - - # the convention is that the compiled primary report will be located in - # AS_BASE/reports/ClassNameReport/ClassNameReport.jasper - def report - self.class.report - end - - # this method compiles our jrxml files into jasper files - def self.compile - StaticAssetFinder.new('reports').find_by_extension(".jrxml").each do |jrxml| - begin - JasperCompileManager.compile_report_to_file(jrxml, jrxml.gsub(".jrxml", ".jasper")) - rescue => e - $stderr.puts "*" * 100 - $stderr.puts "*** JASPER REPORTS ERROR :" - $stderr.puts "*** Unable to compile #{jrxml}" - $stderr.puts "*** #{e.inspect}" - $stderr.puts "*" * 100 - end - end - end - - - # this makes sure all our tempfiles get unlinked... - def self.finalize(obj) - proc { - unless obj.export_file.nil? - obj.export_file.close! - end - obj.datasource.close! - } - end - - def self.report - StaticAssetFinder.new(report_base).find_all( self.name + ".jasper").find do |f| - File.basename(f, '.jasper') == self.name - end - end - - def self.report_base - "reports" - end - - -end diff --git a/backend/app/model/reports/jasper_report_register.rb b/backend/app/model/reports/jasper_report_register.rb deleted file mode 100644 index 888b7092cc..0000000000 --- a/backend/app/model/reports/jasper_report_register.rb +++ /dev/null @@ -1,35 +0,0 @@ -require_relative 'jasper_report' -require_relative 'jdbc_report' -require_relative 'json_report' - - -class JasperReportRegister - - # this registers the reports so they work in the URI - def self.register_reports - begin - Array(StaticAssetFinder.new('reports').find_all("report_config.yml")).each do |config| - begin - yml = YAML.load_file(config) - self.register_report(yml) - end - end - rescue NotFoundException - $stderr.puts("NO JASPER REPORTS FOUND") - end - end - - def self.register_report(opts) - # futz to get the class name correct - if opts["report_type"] == 'json' - ancestor = Object.const_get( "JSONReport" ) - else - ancestor = Object.const_get( "JDBCReport") - end - - report = "#{opts["uri_suffix"].split("_").map { |w| w.capitalize }.join }Report" - klass = Object.const_set( report, Class.new(ancestor) ) - klass.send( :register_report, opts) - end - -end diff --git a/backend/app/model/reports/jdbc_report.rb b/backend/app/model/reports/jdbc_report.rb deleted file mode 100644 index 33567f35dd..0000000000 --- a/backend/app/model/reports/jdbc_report.rb +++ /dev/null @@ -1,80 +0,0 @@ -require_relative 'jasper_report' -require 'csv' -require 'json' - -class JDBCReport < JasperReport - - def default_params - params = {} - params[JsonQueryExecuterFactory::JSON_DATE_PATTERN] ||= "yyyy-MM-dd" - params[JsonQueryExecuterFactory::JSON_NUMBER_PATTERN] ||= "#,##0.##" - params[JsonQueryExecuterFactory::JSON_LOCALE] ||= Locale::ENGLISH - params[JRParameter::REPORT_LOCALE] ||= ::Locale::US - params["repositoryId"] = @repo_id.to_java(:int) - params["basePath"] = @base_path - params - end - - def fill( params = {} ) - params.merge!(default_params) - DB.open(false) do |db| - db.pool.hold do |conn| - @jrprint = JasperFillManager.fill_report(report, java.util.HashMap.new(params), conn ) - end - end - end - - def to_pdf - JasperExportManager.export_report_to_pdf(@jrprint) - end - - def to_html - @export_file = Tempfile.new("location.html") - JasperExportManager.export_report_to_html_file(@jrprint, @export_file.path) - @export_file.rewind - @export_file.read.to_java_bytes - end - - def to_csv - exporter = JRCsvExporter.new - exporter.exporter_input = SimpleExporterInput.new(@jrprint) - @export_file = Tempfile.new(SecureRandom.hex) - exporter.exporter_output = SimpleWriterExporterOutput.new(@export_file.to_outputstream) - exporter.export_report - @export_file.rewind - @export_file.read.to_java_bytes - end - - def to_xlsx - exporter = JRXlsxExporter.new - exporter.exporter_input = SimpleExporterInput.new(@jrprint) - @export_file = Tempfile.new(SecureRandom.hex) - exporter.exporter_output = SimpleOutputStreamExporterOutput.new(@export_file.to_outputstream) - configuration = SimpleXlsxReportConfiguration.new - configuration.one_page_per_sheet = false - exporter.configuration = configuration - exporter.export_report - @export_file.rewind - @export_file.read.to_java_bytes - - end - - def to_json - json = { "results" => [] } - csv = CSV.parse(String.from_java_bytes(to_csv), :headers => true) - csv.each do |row| - result = {} - row.each { |header,val| result[header.downcase] = val unless header.nil? } - json["results"] << result - end - JSON(json).to_java_bytes - end - - def render(format, params = {}) - if [:pdf, :html, :xlsx, :csv, :json ].include?(format) - fill(params) - self.send("to_#{format.to_s}") - end - end - -end diff --git a/backend/app/model/reports/json_report.rb b/backend/app/model/reports/json_report.rb deleted file mode 100644 index a828b9f68d..0000000000 --- a/backend/app/model/reports/json_report.rb +++ /dev/null @@ -1,97 +0,0 @@ -require_relative 'jasper_report' - -class JSONReport < JasperReport - - def initialize(params, job) - @repo_id = params[:repo_id] if params.has_key?(:repo_id) && params[:repo_id] != "" - @base_path = File.join(self.class.report_base, self.class.name ) - @datasource = Tempfile.new(self.class.name + '.data') - - ObjectSpace.define_finalizer( self, self.class.finalize(self) ) - end - - - # there are several ways to attach data to your jasper report. most of them - # don't seem to work very well. One that does it to add a file uri to the - # json.datasource property that's passed as a param. since this works, it - # will be the default. - def load_datasource - @datasource.write(query.to_json) - @datasource.rewind # be kind - @datasource.path - end - - # this is where we load the data. it most likely will be a sequel query - def query - { :locations => [] } - end - - def default_params - params = {} - params[JsonQueryExecuterFactory::JSON_DATE_PATTERN] ||= "yyyy-MM-dd" - params[JsonQueryExecuterFactory::JSON_NUMBER_PATTERN] ||= "#,##0.##" - params[JsonQueryExecuterFactory::JSON_LOCALE] ||= Locale::ENGLISH - params[JRParameter::REPORT_LOCALE] ||= ::Locale::US - params["repositoryId"] = @repo_id - params["basePath"] = @base_path - params - end - - def fill( params = {} ) - params.merge!(default_params) - params["net.sf.jasperreports.json.source"] = load_datasource - - @jrprint = JasperFillManager.fill_report(report, java.util.HashMap.new(params) ) - - end - - def to_pdf - JasperExportManager.export_report_to_pdf(@jrprint) - end - - def to_html - @export_file = Tempfile.new("location.html") - JasperExportManager.export_report_to_html_file(@jrprint, @export_file.path) - @export_file.rewind - @export_file.read.to_java_bytes - end - - def to_csv - exporter = JRCsvExporter.new - exporter.exporter_input = SimpleExporterInput.new(@jrprint) - @export_file = Tempfile.new("location.csv") - exporter.exporter_output = SimpleWriterExporterOutput.new(@export_file.to_outputstream) - exporter.export_report - @export_file.rewind - @export_file.read.to_java_bytes - end - - def to_xlsx - exporter = JRXlsxExporter.new - exporter.exporter_input = SimpleExporterInput.new(@jrprint) - @export_file = Tempfile.new("location.xlsx") - exporter.exporter_output = SimpleOutputStreamExporterOutput.new(@export_file.to_outputstream) - configuration = SimpleXlsxReportConfiguration.new - configuration.one_page_per_sheet = false - exporter.configuration = configuration - exporter.export_report - @export_file.rewind - @export_file.read.to_java_bytes - - end - - def to_json - @datasource.read.to_java_bytes - end - - def render(format, params = {} ) - if format == :json - load_datasource - to_json - elsif [:pdf, :html, :xlsx, :csv ].include?(format) - fill(params) - self.send("to_#{format.to_s}") - end - end - -end diff --git a/backend/app/model/reports/location_holdings_report.rb b/backend/app/model/reports/location_holdings_report.rb deleted file mode 100644 index 8ff4448655..0000000000 --- a/backend/app/model/reports/location_holdings_report.rb +++ /dev/null @@ -1,309 +0,0 @@ -class LocationHoldingsReport < AbstractReport - - register_report({ - :uri_suffix => "location_holdings", - :description => I18n.t('reports.location_holdings.report_title'), - :params => [["locations", "LocationList", "The locations of interest"]] - }) - - include JSONModel - - attr_reader :building, :repository_uri, :start_location, :end_location - - def initialize(params, job) - super - - if ASUtils.present?(params['building']) - @building = params['building'] - elsif ASUtils.present?(params['repository_uri']) - @repository_uri = params['repository_uri'] - - RequestContext.open(:repo_id => JSONModel(:repository).id_for(@repository_uri)) do - unless current_user.can?(:view_repository) - raise AccessDeniedException.new("User does not have access to view the requested repository") - end - end - else - @start_location = Location.get_or_die(JSONModel(:location).id_for(params['location_start']['ref'])) - - if ASUtils.present?(params['location_end']) - @end_location = Location.get_or_die(JSONModel(:location).id_for(params['location_end']['ref'])) - end - end - end - - def title - I18n.t('reports.LocationHoldingsReport') - end - - def headers - [ - 'building', 'floor_and_room', 'location_in_room', - 'location_url', 'location_profile', 'location_barcode', - 'resource_or_accession_id', 'resource_or_accession_title', - 'top_container_indicator', 'top_container_barcode', 'container_profile', - 'ils_item_id', 'ils_holding_id', 'repository' - ] - end - - def processor - { - 'floor_and_room' => proc {|row| floor_and_room(row)}, - 'location_in_room' => proc {|row| location_in_room(row)}, - 'location_url' => proc {|row| location_url(row)}, - } - end - - def query(db) - dataset = if building - building_query(db) - elsif repository_uri - repository_query(db) - elsif start_location && end_location - range_query(db) - else - single_query(db) - end - - # Join location to top containers, repository and (optionally) location and container profiles - dataset = dataset - .left_outer_join(:location_profile_rlshp, :location_id => :location__id) - .left_outer_join(:location_profile, :id => :location_profile_rlshp__location_profile_id) - .join(:top_container_housed_at_rlshp, - :location_id => :location__id, - :top_container_housed_at_rlshp__status => 'current') - .join(:top_container, :id => :top_container_housed_at_rlshp__top_container_id) - .join(:repository, :id => :top_container__repo_id) - .left_outer_join(:top_container_profile_rlshp, :top_container_id => :top_container_housed_at_rlshp__top_container_id) - .left_outer_join(:container_profile, :id => :top_container_profile_rlshp__container_profile_id) - - # A top container can be linked (via subcontainer) to an instance attached - # to an archival object, resource or accession. We'd like to report on the - # ultimate collection of that linkage--the accession or resource tree that - # the top container is linked into. - # - # So, here comes more joins... - dataset = dataset - .left_outer_join(:top_container_link_rlshp, :top_container_id => :top_container__id) - .left_outer_join(:sub_container, :id => :top_container_link_rlshp__sub_container_id) - .left_outer_join(:instance, :id => :sub_container__id) - .left_outer_join(:archival_object, :id => :instance__archival_object_id) - .left_outer_join(:resource, :id => :instance__resource_id) - .left_outer_join(:accession, :id => :instance__accession_id) - .left_outer_join(:resource___resource_via_ao, :id => :archival_object__root_record_id) - - # Used so we can combine adjacent rows for accession/resources linkages - # (i.e. one top container linked to multiple collections) - dataset = dataset.order(:top_container_id) - - dataset = dataset.select(Sequel.as(:location__building, :building), - Sequel.as(:location__floor, :floor), - Sequel.as(:location__room, :room), - Sequel.as(:location__area, :area), - Sequel.as(:location__id, :location_id), - - Sequel.as(:location__coordinate_1_label, :coordinate_1_label), - Sequel.as(:location__coordinate_1_indicator, :coordinate_1_indicator), - Sequel.as(:location__coordinate_2_label, :coordinate_2_label), - Sequel.as(:location__coordinate_2_indicator, :coordinate_2_indicator), - Sequel.as(:location__coordinate_3_label, :coordinate_3_label), - Sequel.as(:location__coordinate_3_indicator, :coordinate_3_indicator), - - Sequel.as(:location_profile__name, :location_profile), - Sequel.as(:location__barcode, :location_barcode), - Sequel.as(:top_container__indicator, :top_container_indicator), - Sequel.as(:top_container__barcode, :top_container_barcode), - Sequel.as(:container_profile__name, :container_profile), - Sequel.as(:top_container__id, :top_container_id), - Sequel.as(:top_container__ils_item_id, :ils_item_id), - Sequel.as(:top_container__ils_holding_id, :ils_holding_id), - Sequel.as(:repository__name, :repository), - - Sequel.as(:resource__title, :resource_title), - Sequel.as(:resource_via_ao__title, :resource_via_ao_title), - Sequel.as(:accession__title, :accession_title), - - Sequel.as(:resource__identifier, :resource_identifier), - Sequel.as(:resource_via_ao__identifier, :resource_via_ao_identifier), - Sequel.as(:accession__identifier, :accession_identifier), - ) - - dataset -end - - def building_query(db) - db[:location].filter(:location__building => building) - end - - def repository_query(db) - repo_id = JSONModel.parse_reference(repository_uri)[:id] - - location_ids = db[:location] - .join(:top_container_housed_at_rlshp, :location_id => :location__id) - .join(:top_container, :top_container__id => :top_container_housed_at_rlshp__top_container_id) - .filter(:top_container__repo_id => repo_id) - .select(:location__id) - - ds = db[:location].filter(:location__id => location_ids) - - # We add a filter at this point to only show holdings for the current - # repository. This works because we know our dataset will be joined with - # the top_container table in our `query` method, and Sequel doesn't mind if - # we add filters for columns that haven't been joined in yet. - # - ds.filter(:top_container__repo_id => repo_id) - end - - def single_query(db) - db[:location].filter(:location__id => start_location.id) - end - - def range_query(db) - # Find the most specific mismatch between the two locations: building -> floor -> room -> area -> c1 -> c2 -> c3 - properties_to_compare = [:building, :floor, :room, :area] - - [1, 2, 3].each do |coordinate| - label = "coordinate_#{coordinate}_label" - if !start_location[label].nil? && start_location[label] == end_location[label] - properties_to_compare << "coordinate_#{coordinate}_indicator".intern - else - break - end - end - - matching_properties = [] - determinant_property = nil - - properties_to_compare.each do |property| - - if start_location[property] && end_location[property] - - if start_location[property] == end_location[property] - # If both locations have the same value for this property, we'll skip it for the purposes of our range calculation - matching_properties << property - else - # But if they have different values, that's what we'll use for the basis of our range - determinant_property = property - break - end - - elsif !start_location[property] && !end_location[property] - # If neither location has a value for this property, skip it - next - - else - # If we hit a property that only one location has a value for, we can't use it for a range calculation - break - end - - end - - if matching_properties.empty? && determinant_property.nil? - # an empty dataset - return db[:location].where { 1 == 0 } - end - - dataset = db[:location] - - matching_properties.each do |property| - dataset = dataset.filter(property => start_location[property]) - end - - if determinant_property - range_start, range_end = [start_location[determinant_property], end_location[determinant_property]].sort - dataset = dataset - .filter("#{determinant_property} >= ?", range_start) - .filter("#{determinant_property} <= ?", range_end) - end - - dataset - end - - def each - collection_identifier_fields = [:resource_identifier, :resource_via_ao_identifier, :accession_identifier] - collection_title_fields = [:resource_title, :resource_via_ao_title, :accession_title] - - DB.open do |db| - dataset = query(db) - - current_entry = nil - enum = dataset.to_enum - - while true - row = next_row(enum) - - if row && current_entry && current_entry[:_top_container_id] == row[:top_container_id] - # This row can be combined with the previous entry - collection_identifier_fields.each do |field| - current_entry['resource_or_accession_id'] << row[field] - end - - collection_title_fields.each do |field| - current_entry['resource_or_accession_title'] << row[field] - end - else - if current_entry - # Yield the old value - current_entry.delete(:_top_container_id) - current_entry['resource_or_accession_id'] = current_entry['resource_or_accession_id'].compact.uniq.map {|s| format_identifier(s)}.join('; ') - current_entry['resource_or_accession_title'] = current_entry['resource_or_accession_title'].compact.uniq.join('; ') - yield current_entry - end - - # If we hit the end of our rows, we're all done - break unless row - - # Otherwise, start a new entry for the next row - current_entry = Hash[headers.map { |h| - val = (processor.has_key?(h)) ? processor[h].call(row) : row[h.intern] - [h, val] - }] - - current_entry['resource_or_accession_id'] = collection_identifier_fields.map {|field| row[field]} - current_entry['resource_or_accession_title'] = collection_title_fields.map {|field| row[field]} - - # Use the top container ID to combine adjacent rows - current_entry[:_top_container_id] = row[:top_container_id] - end - end - end - end - - private - - def next_row(enum) - enum.next - rescue StopIteration - nil - end - - def format_identifier(s) - if ASUtils.blank?(s) - s - else - ASUtils.json_parse(s).compact.join(" -- ") - end - end - - def floor_and_room(row) - [row[:floor], row[:room]].compact.join(', ') - end - - def location_in_room(row) - fields = [row[:area]] - - [1, 2, 3].each do |coordinate| - if row["coordinate_#{coordinate}_label".intern] - fields << ("%s: %s" % [row["coordinate_#{coordinate}_label".intern], - row["coordinate_#{coordinate}_indicator".intern]]) - end - end - - fields.compact.join(', ') - end - - def location_url(row) - JSONModel(:location).uri_for(row[:location_id]) - end - -end diff --git a/backend/app/model/reports/report_manager.rb b/backend/app/model/reports/report_manager.rb index 10086910c1..beea6095ea 100644 --- a/backend/app/model/reports/report_manager.rb +++ b/backend/app/model/reports/report_manager.rb @@ -3,14 +3,13 @@ module ReportManager def self.register_report(report_class, opts) + opts[:code] = report_class.code opts[:model] = report_class opts[:params] ||= [] - opts[:uri_suffix] ||= report_class.name.downcase + Log.warn("Report with code '#{opts[:code]}' already registered") if @@registered_reports.has_key?(opts[:code]) - Log.warn("Report with uri '#{opts[:uri_suffix]}' already registered") if @@registered_reports.has_key?(opts[:uri_suffix]) - - @@registered_reports[opts[:uri_suffix]] = opts + @@registered_reports[opts[:code]] = opts end @@ -28,7 +27,7 @@ def self.included(base) module ClassMethods - def register_report(opts) + def register_report(opts = {}) ReportManager.register_report(self, opts) end diff --git a/backend/app/model/reports/repository_report.rb b/backend/app/model/reports/repository_report.rb deleted file mode 100644 index 0bafd84e81..0000000000 --- a/backend/app/model/reports/repository_report.rb +++ /dev/null @@ -1,34 +0,0 @@ -class RepositoryReport < AbstractReport - register_report({ - :uri_suffix => "repository_report", - :description => "Report on repository records" - }) - - def initialize(params, job) - super - end - - def scope_by_repo_id(dataset) - # repo scope is applied in the query below - dataset - end - - def title - "Repository Report" - end - - def headers - Repository.columns - end - - def processor - { - 'identifier' => proc {|record| ASUtils.json_parse(record[:identifier] || "[]").compact.join("-")}, - } - end - - def query(db) - db[:repository].where( :id => @repo_id) - end - -end diff --git a/backend/app/model/repository.rb b/backend/app/model/repository.rb index 00e60b1602..e6cb4ead66 100644 --- a/backend/app/model/repository.rb +++ b/backend/app/model/repository.rb @@ -1,5 +1,6 @@ class Repository < Sequel::Model(:repository) include ASModel + include Publishable set_model_scope :global corresponds_to JSONModel(:repository) @@ -45,7 +46,7 @@ def after_create "view_repository", "delete_archival_record", "suppress_archival_record", "manage_subject_record", "manage_agent_record", "manage_vocabulary_record", "manage_rde_templates", "manage_container_record", "manage_container_profile_record", - "manage_location_profile_record", "import_records"] + "manage_location_profile_record", "import_records", "cancel_job"] }, { :group_code => "repository-archivists", @@ -85,7 +86,7 @@ def after_create :group_code => "repository-basic-data-entry", :description => "Basic Data Entry users of the #{repo_code} repository", :grants_permissions => ["view_repository", "update_accession_record", "update_resource_record", - "update_digital_object_record"] + "update_digital_object_record", "create_job"] }, { :group_code => "repository-viewers", @@ -147,4 +148,21 @@ def display_string "#{name} (#{repo_code})" end + + def update_from_json(json, opts = {}, apply_nested_records = true) + reindex_required = self.publish != (json['publish'] ? 1 : 0) + + result = super + reindex_repository_records if reindex_required + result + end + + def reindex_repository_records + ASModel.all_models.each do |model| + if model.model_scope(true) == :repository && model.publishable? + model.update_mtime_for_repo_id(self.id) + end + end + end + end diff --git a/backend/app/model/search.rb b/backend/app/model/search.rb index cee4636930..f4d63a5ce0 100644 --- a/backend/app/model/search.rb +++ b/backend/app/model/search.rb @@ -1,8 +1,9 @@ +require_relative 'search_resolver' + class Search + def self.search(params, repo_id) - def self.search(params, repo_id ) - show_suppressed = !RequestContext.get(:enforce_suppression) show_published_only = RequestContext.get(:current_username) === User.PUBLIC_USERNAME @@ -16,24 +17,150 @@ def self.search(params, repo_id ) Solr::Query.create_match_all_query end - query.pagination(params[:page], params[:page_size]). - set_repo_id(repo_id). - set_record_types(params[:type]). + set_repo_id(repo_id). + set_record_types(params[:type]). + show_suppressed(show_suppressed). + show_published_only(show_published_only). + set_excluded_ids(params[:exclude]). + set_filter(params[:filter]). + set_facets(params[:facet], (params[:facet_mincount] || 0)). + set_sort(params[:sort]). + set_root_record(params[:root_record]). + highlighting(params[:hl]). + set_writer_type( params[:dt] || "json" ) + + query.remove_csv_header if ( params[:dt] == "csv" and params[:no_csv_header] ) + + results = Solr.search(query) + + if params[:resolve] + # As with the ArchivesSpace API, resolving a field gives a way of + # returning linked records without having to make multiple queries. + # + # In the case of searching, a resolve parameter like: + # + # &resolve[]=repository:id + # + # will take the (stored) field value for "repository" and search for + # that value in the "id" field of other Solr documents. Any document(s) + # returned will be inserted into the search response under the key + # "_resolved_repository". + # + # Since you might want to resolve a multi-valued field, we'll use the + # following format: + # + # "_resolved_myfield": { + # "/stored/value/1": [{... matched record 1...}, {... matched record 2...}], + # "/stored/value/2": [{... matched record 1...}, {... matched record 2...}] + # } + # + # To avoid the inlined resolved records being unreasonably large, you can + # also specify a custom resolver to be used when rendering the record. + # For example, the query: + # + # &resolve[]=resource:id@compact_resource + # + # will use the "compact_resource" resolver to render the inlined resource + # records. This is defined by `search_resolver_compact_resource.rb`. You + # can define as many of these classes as needed, and they'll be available + # via the API in this same way. + resolver = SearchResolver.new(params[:resolve]) + resolver.resolve(results) + end + + results + end + + def self.records_for_uris(uris, resolve = []) + show_suppressed = !RequestContext.get(:enforce_suppression) + show_published_only = RequestContext.get(:current_username) === User.PUBLIC_USERNAME + + boolean_query = JSONModel.JSONModel(:boolean_query) + .from_hash('op' => 'OR', + 'subqueries' => uris.map {|uri| + JSONModel.JSONModel(:field_query) + .from_hash('field' => 'id', + 'value' => uri, + 'literal' => true) + .to_hash + }) + + query = Solr::Query.create_advanced_search(JSONModel.JSONModel(:advanced_query).from_hash('query' => boolean_query)) + + query.pagination(1, uris.length). show_suppressed(show_suppressed). - show_published_only(show_published_only). - set_excluded_ids(params[:exclude]). - set_filter_terms(params[:filter_term]). - set_simple_filters(params[:simple_filter]). - set_facets(params[:facet]). - set_sort(params[:sort]). - set_root_record(params[:root_record]). - highlighting(params[:hl]). - set_writer_type( params[:dt] || "json" ) - - query.remove_csv_header if ( params[:dt] == "csv" and params[:no_csv_header] ) - - Solr.search(query) + show_published_only(show_published_only) + + results = Solr.search(query) + + resolver = SearchResolver.new(resolve) + resolver.resolve(results) + + results + end + + def self.record_type_counts(record_types, for_repo_uri = nil) + show_suppressed = !RequestContext.get(:enforce_suppression) + show_published_only = RequestContext.get(:current_username) === User.PUBLIC_USERNAME + + result = {} + + repos_of_interest = if for_repo_uri + [for_repo_uri] + else + Repository.filter(:hidden => 0).select(:id).map do |row| + repo_id = row[:id] + JSONModel.JSONModel(:repository).uri_for(repo_id) + end + end + + repos_of_interest.each do |repo_uri| + result[repo_uri] ||= {} + + record_types.each do |record_type| + boolean_query = JSONModel.JSONModel(:boolean_query) + .from_hash('op' => 'AND', + 'subqueries' => [ + JSONModel.JSONModel(:boolean_query).from_hash('op' => 'OR', + 'subqueries' => [ + JSONModel.JSONModel(:field_query) + .from_hash('field' => 'used_within_repository', + 'value' => repo_uri, + 'literal' => true).to_hash, + JSONModel.JSONModel(:field_query) + .from_hash('field' => 'repository', + 'value' => repo_uri, + 'literal' => true).to_hash + ]), + JSONModel.JSONModel(:field_query) + .from_hash('field' => 'types', + 'value' => record_type, + 'literal' => true).to_hash, + JSONModel.JSONModel(:field_query) + .from_hash('field' => 'published', + 'value' => 'true', + 'literal' => true).to_hash + + ]) + + query = Solr::Query.create_advanced_search(JSONModel.JSONModel(:advanced_query).from_hash('query' => boolean_query)) + query.pagination(1, 1). + show_suppressed(show_suppressed). + show_published_only(show_published_only) + + hits = Solr.search(query) + + result[repo_uri][record_type] = hits['total_hits'] + end + end + + if for_repo_uri + # We're just targeting a single repo + result.values.first + else + result + end end def self.search_csv( params, repo_id ) diff --git a/backend/app/model/search_resolver.rb b/backend/app/model/search_resolver.rb new file mode 100644 index 0000000000..eb5a50c3b1 --- /dev/null +++ b/backend/app/model/search_resolver.rb @@ -0,0 +1,90 @@ +class SearchResolver + + # Theoretically someone might resolve a field that matches an unbounded number + # of records, and this could cause an OOM. Set an upper bound. + MAX_RESOLVE_RESULTS = AppConfig[:max_page_size] * 2 + + def initialize(resolve_definitions) + @resolve_definitions = resolve_definitions.map {|s| ResolveDefinition.parse(s)} + end + + def resolve(results) + @resolve_definitions.each do |resolve_def| + source_field = resolve_def.source_field + target_field = resolve_def.target_field + custom_resolver = resolve_def.custom_resolver + + # Build up and run a Solr query to pull back the documents we'll be inlining + search_terms = results['results'].map {|doc| doc[source_field]}.compact.flatten + + unless search_terms.empty? + boolean_query = JSONModel.JSONModel(:boolean_query) + .from_hash('op' => 'OR', + 'subqueries' => search_terms.map {|term| + JSONModel.JSONModel(:field_query) + .from_hash('field' => target_field, + 'value' => term, + 'literal' => true) + .to_hash + }) + + query = JSONModel.JSONModel(:advanced_query).from_hash('query' => boolean_query) + + resolved_results = Solr.search(Solr::Query.create_advanced_search(query).pagination(1, MAX_RESOLVE_RESULTS)) + + if resolved_results['total_hits'] > MAX_RESOLVE_RESULTS + Log.warn("Resolve query hit MAX_RESOLVE_RESULTS. Result set may be incomplete: #{query.to_hash.inspect}") + end + + # Insert the resolved records into our original result set. + results['results'].each do |result| + resolved = resolved_results['results'].map {|resolved| + key = resolved[target_field] + if ASUtils.wrap(result[source_field]).include?(key) + {key => [SearchResolver.resolver_for(custom_resolver).resolve(resolved)]} + end + }.compact + + # Merge our entries into a single hash keyed on `key` + result["_resolved_#{source_field}"] = resolved.reduce {|merged, elt| merged.merge(elt) {|key, coll1, coll2| coll1 + coll2}} + end + end + end + end + + ResolveDefinition = Struct.new(:source_field, :target_field, :custom_resolver) do + def self.parse(resolve_def) + (source_field, target_field, custom_resolver) = resolve_def.split(/[:@]/) + + unless source_field && target_field + raise "Resolve request parameter not well-formed: #{resolve_def}. Should be source_field:target_field" + end + + new(source_field, target_field, custom_resolver) + end + end + + def self.add_custom_resolver(name, resolver_class) + @custom_resolvers ||= {} + @custom_resolvers[name] = resolver_class + end + + def self.resolver_for(name) + if name + clz = @custom_resolvers.fetch(name) { + raise "Unrecognized search resolver: #{name}" + } + + clz.new + else + PassThroughResolver.new + end + end + + class PassThroughResolver + def resolve(record) + record + end + end + +end diff --git a/backend/app/model/search_resolver_compact_resource.rb b/backend/app/model/search_resolver_compact_resource.rb new file mode 100644 index 0000000000..1de910f302 --- /dev/null +++ b/backend/app/model/search_resolver_compact_resource.rb @@ -0,0 +1,13 @@ +class SearchResolverCompactResource + + FIELDS_TO_KEEP = ['id_0', 'id_1', 'id_2', 'id_3', 'level', 'other_level', 'title', 'uri', 'publish'] + + # Really just including this as a demo. Let's parse the JSON and extract a few fields. + def resolve(record) + resource = ASUtils.json_parse(record['json']) + resource.select {|key, _| FIELDS_TO_KEEP.include?(key)} + end + + SearchResolver.add_custom_resolver('compact_resource', self) + +end diff --git a/backend/app/model/session.rb b/backend/app/model/session.rb index 6441e5b152..cfd6d00451 100644 --- a/backend/app/model/session.rb +++ b/backend/app/model/session.rb @@ -5,10 +5,59 @@ class Session SESSION_ID_LENGTH = 32 - attr_reader :id + # If it's worth doing it's worth overdoing! + # + # For really small AJAX-driven lookups, like nodes and waypoints, sometimes + # touching the user's session (with its associated database commit) was adding + # 5-10x to the response time. Upsetting! + # + # Since touching sessions is very common, but not really mission critical, + # offload the work to a background thread that will periodically update them. + + UPDATE_FREQUENCY_SECONDS = 5 + + def self.init + @sessions_to_update = Queue.new + + @session_touch_thread = Thread.new do + while true + begin + self.touch_pending_sessions + rescue + Log.exception($!) + end + + sleep UPDATE_FREQUENCY_SECONDS + end + end + end + + def self.touch_pending_sessions(now = Time.now) + sessions = [] + + while !@sessions_to_update.empty? + sessions << @sessions_to_update.pop + end + + unless sessions.empty? + DB.open do |db| + db[:session] + .filter(:session_id => sessions.map {|id| Digest::SHA1.hexdigest(id) }.uniq) + .update(:system_mtime => now) + end + end + end + + def self.touch_session(id) + @sessions_to_update << id + end - def initialize(sid = nil, store = nil) + attr_reader :id, :system_mtime + + def initialize(sid = nil, store = nil, system_mtime = nil) + now = Time.now + if not sid # Create a new session in the DB DB.open do |db| @@ -19,7 +68,7 @@ def initialize(sid = nil, store = nil) completed = DB.attempt { db[:session].insert(:session_id => Digest::SHA1.hexdigest(sid), :session_data => [Marshal.dump({})].pack("m*"), - :system_mtime => Time.now) + :system_mtime => now) true }.and_if_constraint_fails { # Retry with a different session ID. @@ -30,21 +79,26 @@ def initialize(sid = nil, store = nil) end @id = sid + @system_mtime = now @store = {} end else @id = sid @store = store + @system_mtime = system_mtime end end def self.find(sid) DB.open do |db| - session_data = db[:session].filter(:session_id => Digest::SHA1.hexdigest(sid)).get(:session_data) + row = db[:session] + .filter(:session_id => Digest::SHA1.hexdigest(sid)) + .select(:session_data, :system_mtime) + .first - if session_data - Session.new(sid, Marshal.load(session_data.unpack("m*").first)) + if row + Session.new(sid, Marshal.load(row[:session_data].unpack("m*").first), row[:system_mtime]) else nil end @@ -90,21 +144,11 @@ def save def touch - DB.open do |db| - db[:session] - .filter(:session_id => Digest::SHA1.hexdigest(@id)) - .update(:system_mtime => Time.now) - end + self.class.touch_session(@id) end def age - system_mtime = 0 - DB.open do |db| - system_mtime = db[:session] - .filter(:session_id => Digest::SHA1.hexdigest(@id)) - .get(:system_mtime) - end (Time.now - system_mtime).to_i end diff --git a/backend/app/model/solr.rb b/backend/app/model/solr.rb index 17cbc6afe9..09cd35faac 100644 --- a/backend/app/model/solr.rb +++ b/backend/app/model/solr.rb @@ -39,41 +39,24 @@ def self.create_advanced_search(advanced_query_json) end - def self.construct_advanced_query_string(advanced_query) + def self.construct_advanced_query_string(advanced_query, use_literal = false) if advanced_query.has_key?('subqueries') - subqueries = advanced_query['subqueries'].map {|subq| - construct_advanced_query_string(subq) - }.join(" #{advanced_query['op']} ") + clauses = advanced_query['subqueries'].map {|subq| + construct_advanced_query_string(subq, use_literal) + } - "(#{subqueries})" - else - prefix = advanced_query['negated'] ? "-" : "" - field = AdvancedSearch.solr_field_for(advanced_query['field']) - - if advanced_query["jsonmodel_type"] == "date_field_query" - if advanced_query["comparator"] == "lesser_than" - value = "[* TO #{advanced_query["value"]}T00:00:00Z-1MILLISECOND]" - elsif advanced_query["comparator"] == "greater_than" - value = "[#{advanced_query["value"]}T00:00:00Z+1DAY TO *]" - else # advanced_query["comparator"] == "equal" - value = "[#{advanced_query["value"]}T00:00:00Z TO #{advanced_query["value"]}T00:00:00Z+1DAY-1MILLISECOND]" - end - elsif advanced_query["jsonmodel_type"] == "field_query" && advanced_query["literal"] - value = "(\"#{solr_escape(advanced_query['value'])}\")" - else - value = "(#{advanced_query['value']})" + # Solr doesn't allow purely negative expression groups, so we add a + # match all query to compensate when we hit one of these. + if advanced_query['subqueries'].all? {|subquery| subquery['negated']} + clauses << '*:*' end - "#{prefix}#{field}:#{value}" - end - end + subqueries = clauses.join(" #{advanced_query['op']} ") - - SOLR_CHARS = '+-&|!(){}[]^"~*?:\\/' - - def self.solr_escape(s) - pattern = Regexp.quote(SOLR_CHARS) - s.gsub(/([#{pattern}])/, '\\\\\1') + "(#{subqueries})" + else + AdvancedQueryString.new(advanced_query, use_literal).to_solr_s + end end @@ -165,23 +148,11 @@ def set_excluded_ids(ids) end - def set_filter_terms(filter_terms) - unless Array(filter_terms).empty? - filter_terms.map{|str| ASUtils.json_parse(str)}.each{|json| - json.each {|facet, term| - add_solr_param(:fq, self.class.term_query(facet.strip, term.to_s.strip)) - } - } - end - - self - end - - def set_simple_filters(filter_terms) - unless Array(filter_terms).empty? - filter_terms.map{|str| - add_solr_param(:fq, str.strip ) - } + def set_filter(advanced_query) + if advanced_query + query_string = self.class.construct_advanced_query_string(advanced_query['query'], + use_literal = true) + add_solr_param(:fq, query_string) end self @@ -194,11 +165,13 @@ def show_suppressed(value) end - def set_facets(fields) + def set_facets(fields, mincount = 0) if fields @facet_fields = fields end + @facet_mincount = mincount + self end @@ -261,6 +234,7 @@ def to_solr_url unless @facet_fields.empty? add_solr_param(:"facet.field", @facet_fields) add_solr_param(:"facet.limit", AppConfig[:solr_facet_limit]) + add_solr_param(:"facet.mincount", @facet_mincount) end if @query_type == :edismax @@ -327,7 +301,7 @@ def self.search(query) result['total_hits'] = json['response']['numFound'] result['results'] = json['response']['docs'].map {|doc| - doc['uri'] = doc['id'] + doc['uri'] ||= doc['id'] doc['jsonmodel_type'] = doc['primary_type'] doc } diff --git a/backend/app/model/subject.rb b/backend/app/model/subject.rb index de06c9b28b..b879fd9607 100644 --- a/backend/app/model/subject.rb +++ b/backend/app/model/subject.rb @@ -105,6 +105,14 @@ def update_from_json(json, opts = {}, apply_nested_records = true) def self.sequel_to_jsonmodel(objs, opts = {}) jsons = super + if opts[:calculate_linked_repositories] + subjects_to_repositories = GlobalRecordRepositoryLinkages.new(self, :subject).call(objs) + + jsons.zip(objs).each do |json, obj| + json.used_within_repositories = subjects_to_repositories.fetch(obj, []).map {|repo| repo.uri} + end + end + jsons.zip(objs).each do |json, obj| json.vocabulary = uri_for(:vocabulary, obj.vocab_id) end diff --git a/backend/app/model/top_container.rb b/backend/app/model/top_container.rb index cf55561d33..938127de4f 100644 --- a/backend/app/model/top_container.rb +++ b/backend/app/model/top_container.rb @@ -12,7 +12,8 @@ class TopContainer < Sequel::Model(:top_container) include RestrictionCalculator - + SERIES_LEVELS = ['series'] + OTHERLEVEL_SERIES_LEVELS = ['accession'] def validate validates_unique([:repo_id, :barcode], @@ -36,73 +37,111 @@ def format_barcode end - # For Archival Objects, the series is the topmost record in the tree. - def tree_top(obj) - if obj.respond_to?(:series) - obj.series - else - nil - end + def self.linked_instance_ds + TopContainer + .join(:top_container_link_rlshp, :top_container_link_rlshp__top_container_id => :top_container__id) + .join(:sub_container, :sub_container__id => :top_container_link_rlshp__sub_container_id) + .join(:instance, :instance__id => :sub_container__instance_id) end - # return all archival records linked to this top container - def linked_archival_records - related_records(:top_container_link).map {|subcontainer| - linked_archival_record_for(subcontainer) - }.compact.uniq {|obj| obj.uri} + def collections + @collections ||= calculate_collections end - - def self.linked_instance_ds - db[:instance]. - join(:sub_container, :instance_id => :instance__id). - join(:top_container_link_rlshp, :sub_container_id => :sub_container__id). - join(:top_container, :id => :top_container_link_rlshp__top_container_id) + def calculate_collections + result = [] + + # Resource linked directly + resource_ids = [] + resource_ids += Resource + .join(:instance, :instance__resource_id => :resource__id) + .join(:sub_container, :sub_container__instance_id => :instance__id) + .join(:top_container_link_rlshp, :top_container_link_rlshp__sub_container_id => :sub_container__id) + .filter(:top_container_link_rlshp__top_container_id => self.id) + .select(:resource__id) + .distinct + .all.map{|row| row[:id]} + + # Resource linked via AO + resource_ids += Resource + .join(:archival_object, :archival_object__root_record_id => :resource__id) + .join(:instance, :instance__archival_object_id => :archival_object__id) + .join(:sub_container, :sub_container__instance_id => :instance__id) + .join(:top_container_link_rlshp, :top_container_link_rlshp__sub_container_id => :sub_container__id) + .filter(:top_container_link_rlshp__top_container_id => self.id) + .select(:resource__id) + .distinct + .all.map{|row| row[:id]} + + result += Resource + .filter(:id => resource_ids.uniq) + .select_all(:resource) + .all + + result += Accession + .join(:instance, :instance__accession_id => :accession__id) + .join(:sub_container, :sub_container__instance_id => :instance__id) + .join(:top_container_link_rlshp, :top_container_link_rlshp__sub_container_id => :sub_container__id) + .filter(:top_container_link_rlshp__top_container_id => self.id) + .select_all(:accession) + .all + + result.uniq {|obj| [obj.class, obj.id]} end - def linked_archival_record_for(subcontainer) - # Find its linked instance - instance = Instance[subcontainer.instance_id] - - return nil unless instance - - # Find the record that links to that instance - ASModel.all_models.each do |model| - next unless model.associations.include?(:instance) + def series + @series ||= calculate_series + end - association = model.association_reflection(:instance) + def calculate_series + linked_aos = ArchivalObject + .join(:instance, :instance__archival_object_id => :archival_object__id) + .join(:sub_container, :sub_container__instance_id => :instance__id) + .join(:top_container_link_rlshp, :top_container_link_rlshp__sub_container_id => :sub_container__id) + .filter(:top_container_link_rlshp__top_container_id => self.id) + .select(:archival_object__id) + + # Find the top-level archival objects of our selected records. + # Unfortunately there's no easy way to do this besides walking back up the + # tree. + top_level_aos = walk_to_top_level_aos(linked_aos.map {|row| row[:id]}) + + ArchivalObject + .join(:enumeration_value, {:level_enum__id => :archival_object__level_id}, + :table_alias => :level_enum) + .filter(:archival_object__id => top_level_aos) + .exclude(:archival_object__component_id => nil) + .where { Sequel.|( + { :level_enum__value => SERIES_LEVELS }, + Sequel.&({ :level_enum__value => 'otherlevel'}, + { Sequel.function(:lower, :other_level) => OTHERLEVEL_SERIES_LEVELS })) + }.select_all(:archival_object) + end - key = association[:key] - if instance[key] - return model[instance[key]] - end - end - end + def walk_to_top_level_aos(ao_ids) + result = [] + id_set = ao_ids + while !id_set.empty? + next_id_set = [] - def collections - linked_archival_records.map {|obj| - if obj.respond_to?(:series) - # An Archival Object - if obj.root_record_id - obj.class.root_model[obj.root_record_id] + ArchivalObject.filter(:id => id_set).select(:id, :parent_id).each do |row| + if row[:parent_id].nil? + # This one's a top-level record + result << row[:id] else - # An Archival Object without a resource. Doesn't really happen in - # normal usage, but the data model does support this... - nil + # Keep looking + next_id_set << row[:parent_id] end - else - obj - end - }.compact.uniq {|obj| obj.uri} - end + id_set = next_id_set + end + end - def series - linked_archival_records.map {|record| tree_top(record)}.compact.uniq {|obj| obj.uri} + result end @@ -219,7 +258,7 @@ def update_from_json(json, opts = {}, apply_nested_records = true) def reindex_linked_records - self.class.update_mtime_for_ids([self.id]) + self.class.update_mtime_for_ids([self.id]) end def self.search_stream(params, repo_id, &block) @@ -235,10 +274,12 @@ def self.search_stream(params, repo_id, &block) query.pagination(1, max_results). set_repo_id(repo_id). set_record_types(params[:type]). - set_filter_terms(params[:filter_term]). - set_simple_filters(params[:simple_filter]). - set_facets(params[:facet]) + set_facets(params[:facet]). + set_filter(params[:filter]) + if params[:filter_term] + query.set_filter(AdvancedQueryBuilder.from_json_filter_terms(params[:filter_term])) + end query.add_solr_param(:qf, "series_identifier_u_stext collection_identifier_u_stext") diff --git a/backend/app/views/reports/generic_listing.erb b/backend/app/views/reports/generic_listing.erb new file mode 100644 index 0000000000..fb2e26fefb --- /dev/null +++ b/backend/app/views/reports/generic_listing.erb @@ -0,0 +1,25 @@ +
+ +

<%= h @report.title %>

+
+ + + + + <% @report.headers.each do |heading| %> + + <% end %> + + + + <% @report.each do |record| %> + + <% @report.headers.each do |heading| %> + + <% end %> + + <% end %> + +
<%= transform_text(heading.to_s) %>
+ <%= transform_text(record[heading].to_s) %> +
diff --git a/backend/app/views/reports/report.erb b/backend/app/views/reports/report.erb index 85f42389c6..7484939a6d 100644 --- a/backend/app/views/reports/report.erb +++ b/backend/app/views/reports/report.erb @@ -1,10 +1,19 @@ +<% if layout? %> + +<% +if @report.template == 'generic_listing.erb' + # Listing template should be printed as landscape to fit on the page. + @report.orientation = 'landscape' +end +%> + - <% - # The HTML to PDF library doesn't currently support the "break-word" CSS - # property that would let us force a linebreak for long strings and URIs. - # Without that, we end up having our tables chopped off, which makes them - # not-especially-useful. - # - # Newer versions of the library might fix this issue, but it appears that the - # licence of the newer version is incompatible with the current ArchivesSpace - # licence. - # - # So, we wrap runs of characters in their own span tags to give the renderer - # a hint on where to place the line breaks. Pretty terrible, but it works. - # - if report.format === 'pdf' - transform_text = proc {|s| - escaped = CGI.escapeHTML(s) - - # Exciting regexp time! We break our string into "tokens", which are either: - # - # - A single whitespace character - # - A HTML-escaped character (like '&') - # - A run of between 1 and 5 letters - # - # Each token is then wrapped in a span, ensuring that we don't go too - # long without having a spot to break a word if needed. - # - escaped.scan(/[\s]|&.*;|[^\s]{1,5}/).map {|token| - if token.start_with?("&") || token =~ /\A[\s]\Z/ - # Don't mess with & and friends, nor whitespace - token - else - "#{token}" - end - }.join("") - } - else - transform_text = proc {|s| CGI.escapeHTML(s) } - end - %> - - -
- -

<%= report.title %>

-
- - - - - <% report.headers.each do |heading| %> - - <% end %> - - - - <% report.each do |record| %> - - <% report.headers.each do |heading| %> - - <% end %> - - <% end %> - -
<%= transform_text.call(heading) %>
- <%= transform_text.call(record[heading].to_s) %> -
+ + <%= render(@report.template) %> - <% if report.format === 'html' or report.format === 'pdf' %> + <% if @report.format === 'html' or @report.format === 'pdf' %> <% time = DateTime.now %> <% end %> + +<% else %> + + <%= render(@report.template) %> +<% end %> diff --git a/backend/scripts/bootstrap_rvm.sh b/backend/scripts/bootstrap_rvm.sh deleted file mode 100755 index ecdafed553..0000000000 --- a/backend/scripts/bootstrap_rvm.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -e - -rvm install jruby - -case "$GEM_HOME" in - *jruby*) - JRUBY_OPTS="--1.9" ; export JRUBY_OPTS - ;; -esac - -if ! command -v bundle ; then - gem install bundler -fi - -bundle - -bundle show - diff --git a/backend/scripts/db_migrate_rvm.sh b/backend/scripts/db_migrate_rvm.sh deleted file mode 100755 index c7e253a570..0000000000 --- a/backend/scripts/db_migrate_rvm.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -base="`dirname $0`" - -JRUBY_OPTS="--1.9" -export JRUBY_OPTS - -export RUBYLIB=$base/../app/lib:$RUBYLIB - -jruby $base/../build/scripts/migrate_db.rb \ No newline at end of file diff --git a/backend/scripts/devserver_rvm.sh b/backend/scripts/devserver_rvm.sh deleted file mode 100755 index 9b7d5875d1..0000000000 --- a/backend/scripts/devserver_rvm.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -base="`dirname $0`" - -JRUBY_OPTS="--1.9" -export JRUBY_OPTS - -export RUBYLIB=$base/../app/lib:$RUBYLIB - -jruby $base/../app/main.rb \ No newline at end of file diff --git a/backend/spec/asmodel_object_graph_spec.rb b/backend/spec/asmodel_object_graph_spec.rb index dbfea86e9c..26f0d23d6f 100644 --- a/backend/spec/asmodel_object_graph_spec.rb +++ b/backend/spec/asmodel_object_graph_spec.rb @@ -24,7 +24,7 @@ it "can produce a simple object graph from a top-level tree" do resource = create(:resource, :repo_id => $repo_id) - top_ao = create(:archival_object, :root_record_id => resource.id, :repo_id => $repo_id) + top_ao = create(:archival_object, :root_record_id => resource.id, :repo_id => $repo_id, :position => 0) count = 5 diff --git a/backend/spec/common_jsonmodel_spec.rb b/backend/spec/common_jsonmodel_spec.rb index 38fb98aedd..0a7fadd20b 100644 --- a/backend/spec/common_jsonmodel_spec.rb +++ b/backend/spec/common_jsonmodel_spec.rb @@ -436,7 +436,7 @@ threads << Thread.new do 1000.times do - build(:json_archival_object) + build(:json_agent_person) end :ok diff --git a/backend/spec/common_mixed_content_parser_spec.rb b/backend/spec/common_mixed_content_parser_spec.rb index 1e9069eccd..75f9fe109b 100644 --- a/backend/spec/common_mixed_content_parser_spec.rb +++ b/backend/spec/common_mixed_content_parser_spec.rb @@ -24,4 +24,21 @@ converted.should eq("What the & 'heck' ok?"); end + it "converts emph element correctly", :skip_db_open do + text = "emph text" + + converted = MixedContentParser.parse(text, "http://example.com", {:wrap_blocks => false}) + + converted.should eq("emph text"); + end + + + it "converts title element correctly", :skip_db_open do + text = "title text" + + converted = MixedContentParser.parse(text, "http://example.com", {:wrap_blocks => false}) + + converted.should eq("title text"); + end + end diff --git a/backend/spec/controller_exports_spec.rb b/backend/spec/controller_exports_spec.rb index d0a208f702..dfa032e47c 100644 --- a/backend/spec/controller_exports_spec.rb +++ b/backend/spec/controller_exports_spec.rb @@ -72,14 +72,13 @@ aos = [] ["earth", "australia", "canberra"].each do |name| - ao = create(:json_archival_object, {:title => "archival object: #{name}"}) + ao = create(:json_archival_object, {:title => "archival object: #{name}", + :resource => {:ref => resource.uri}}) if not aos.empty? ao.parent = {:ref => aos.last.uri} ao.publish = false end - ao.resource = {:ref => resource.uri} - ao.save aos << ao end @@ -97,14 +96,14 @@ aos = [] ["earth", "australia", "canberra"].each do |name| - ao = create(:json_archival_object, {:title => "archival object: #{name}"}) + ao = create(:json_archival_object, {:title => "archival object: #{name}", + :resource => {:ref => resource.uri}}) + if not aos.empty? ao.parent = {:ref => aos.last.uri} ao.publish = false end - ao.resource = {:ref => resource.uri} - ao.save aos << ao end @@ -155,4 +154,37 @@ resp.should match(/#{dig.title}<\/title>/) end + + it "gives you metadata for any kind of export" do + # agent exports + agent = create(:json_agent_person).id + check_metadata("archival_contexts/people/#{agent}.xml") + agent = create(:json_agent_family).id + check_metadata("archival_contexts/families/#{agent}.xml") + agent = create(:json_agent_corporate_entity).id + check_metadata("archival_contexts/corporate_entities/#{agent}.xml") + agent = create(:json_agent_software).id + check_metadata("archival_contexts/softwares/#{agent}.xml") + + # resource exports + res = create(:json_resource, :publish => true).id + check_metadata("resource_descriptions/#{res}.xml") + check_metadata("resources/marc21/#{res}.xml") + check_metadata("resource_labels/#{res}.tsv") + + # digital object exports + dig = create(:json_digital_object).id + check_metadata("digital_objects/mods/#{dig}.xml") + check_metadata("digital_objects/mets/#{dig}.xml") + check_metadata("digital_objects/dublin_core/#{dig}.xml") + end + + + def check_metadata(export_uri) + get "/repositories/#{$repo_id}/#{export_uri}/metadata" + resp = ASUtils.json_parse(last_response.body) + resp.has_key?('mimetype').should be true + resp.has_key?('filename').should be true + end + end diff --git a/backend/spec/controller_rest_spec.rb b/backend/spec/controller_rest_spec.rb index e6b3e2fb9a..2ef6efd3e7 100644 --- a/backend/spec/controller_rest_spec.rb +++ b/backend/spec/controller_rest_spec.rb @@ -112,7 +112,7 @@ it "supports querying Endpoints" do endpoint = RESTHelpers::Endpoint.get("/moo") - endpoint['method'].should eq(:get) + endpoint['methods'].should eq([:get]) endpoint['uri'].should eq('/moo') end diff --git a/backend/spec/controller_search_spec.rb b/backend/spec/controller_search_spec.rb index 0f251433ad..16a786eb10 100644 --- a/backend/spec/controller_search_spec.rb +++ b/backend/spec/controller_search_spec.rb @@ -31,4 +31,18 @@ end + + describe "Endpoints" do + + it "responds to GET requests" do + get '/search' + last_response.status.should_not eq(404) + end + + it "responds to POST requests" do + post '/search' + last_response.status.should_not eq(404) + end + + end end diff --git a/backend/spec/factories.rb b/backend/spec/factories.rb index 641a50b53a..6444aa22b3 100644 --- a/backend/spec/factories.rb +++ b/backend/spec/factories.rb @@ -198,6 +198,7 @@ def JSONModel(key) ref_id { generate(:alphanumstr) } level { generate(:level) } title { "Archival Object #{generate(:generic_title)}" } + resource { {'ref' => create(:json_resource).uri} } end factory :json_archival_object_normal, class: JSONModel(:archival_object) do @@ -206,6 +207,7 @@ def JSONModel(key) title { "Archival Object #{generate(:generic_title)}" } extents { few_or_none(:json_extent) } dates { few_or_none(:json_date) } + resource { {'ref' => create(:json_resource).uri} } end factory :json_classification, class: JSONModel(:classification) do @@ -218,6 +220,7 @@ def JSONModel(key) identifier { generate(:alphanumstr) } title { "Classification #{generate(:generic_title)}" } description { generate(:generic_description) } + classification { {'ref' => create(:json_classification).uri} } end factory :json_note_index, class: JSONModel(:note_index) do @@ -297,7 +300,7 @@ def JSONModel(key) factory :json_top_container, class: JSONModel(:top_container) do indicator { generate(:alphanumstr) } type { generate(:container_type) } - barcode { generate(:alphanumstr)[0..4] } + barcode { SecureRandom.hex } ils_holding_id { generate(:alphanumstr) } ils_item_id { generate(:alphanumstr) } exported_to_ils { Time.now.iso8601 } @@ -359,6 +362,7 @@ def JSONModel(key) factory :json_digital_object_component, class: JSONModel(:digital_object_component) do component_id { generate(:alphanumstr) } title { "Digital Object Component #{generate(:generic_title)}" } + digital_object { {'ref' => create(:json_digital_object).uri} } end factory :json_event, class: JSONModel(:event) do @@ -558,7 +562,7 @@ def JSONModel(key) end factory :json_job, class: JSONModel(:job) do - job_type { ['import_job', 'find_and_replace_job', 'print_to_pdf_job'].sample } + job { build(:json_import_job) } end factory :json_import_job, class: JSONModel(:import_job) do diff --git a/backend/spec/lib_ead_converter_spec.rb b/backend/spec/lib_ead_converter_spec.rb index cb95cf13e1..f54a8fa9c1 100644 --- a/backend/spec/lib_ead_converter_spec.rb +++ b/backend/spec/lib_ead_converter_spec.rb @@ -13,6 +13,24 @@ def my_converter let (:test_doc_1) { src = <<ANEAD +<ead> + <frontmatter> + <titlepage> + <titleproper>A test resource</titleproper> + </titlepage> + </frontmatter> + <archdesc level="collection" audience="internal"> + <did> + <unittitle>一般行政文件 [2]</unittitle> + <unitid>Resource.ID.AT</unitid> + <unitdate normal="1907/1911" era="ce" calendar="gregorian" type="inclusive">1907-1911</unitdate> + <physdesc> + <extent>5.0 Linear feet</extent> + <extent>Resource-ContainerSummary-AT</extent> + </physdesc> + </did> + </archdesc> +<dsc> <c id="1" level="file"> <unittitle>oh well<unitdate normal="1907/1911" era="ce" calendar="gregorian" type="inclusive">1907-1911</unitdate></unittitle> <container id="cid1" type="Box" label="Text (B@RC0D3 )">1</container> @@ -23,6 +41,8 @@ def my_converter <controlaccess><persname rules="dacs" source='local' id='thesame'>Art, Makah</persname></controlaccess> </c> </c> +</dsc> +</ead> ANEAD get_tempfile_path(src) @@ -35,7 +55,7 @@ def my_converter converter.run parsed = JSON(IO.read(converter.get_output_path)) - parsed.length.should eq(3) + parsed.length.should eq(4) parsed.find{|r| r['ref_id'] == '1'}['instances'][0]['container']['type_2'].should eq('Folder') end @@ -54,7 +74,7 @@ def my_converter converter.run parsed = JSON(IO.read(converter.get_output_path)) - parsed.length.should eq(3) + parsed.length.should eq(4) parsed.find{|r| r['ref_id'] == '1'}['title'].should eq('oh well') parsed.find{|r| r['ref_id'] == '1'}['dates'][0]['expression'].should eq("1907-1911") @@ -1017,6 +1037,19 @@ def doc3 describe "Mapping physdesc tags" do def test_doc src = <<ANEAD +<ead> + <archdesc level="collection" audience="internal"> + <did> + <unittitle>一般行政文件 [2]</unittitle> + <unitid>Resource.ID.AT</unitid> + <unitdate normal="1907/1911" era="ce" calendar="gregorian" type="inclusive">1907-1911</unitdate> + <physdesc> + <extent>5.0 Linear feet</extent> + <extent>Resource-ContainerSummary-AT</extent> + </physdesc> + </did> + </archdesc> +<dsc> <c> <did> <unittitle>DIMENSIONS test </unittitle> @@ -1027,6 +1060,8 @@ def test_doc </physdesc> </did> </c> +</dsc> +</ead> ANEAD get_tempfile_path(src) diff --git a/backend/spec/lib_json_report_spec.rb b/backend/spec/lib_json_report_spec.rb deleted file mode 100644 index f3cee53630..0000000000 --- a/backend/spec/lib_json_report_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -require 'spec_helper' - -describe 'JSON Jasper Report model' do - - before(:all) do - opts = { "report_type" => "json", - "uri_suffix" => "locations", - "description" => "a stupid lil test", - "query" => " - results = nil - DB.open do |db| - locations = db[:location].select(:building, :title, :floor, :room, :area, :barcode, :classification, :id ).all - resources = db[:location]. - join(:housed_at_rlshp, :location_id => :location__id). - join(:container, :id => :housed_at_rlshp__container_id). - join(:instance,{ :id => :container__instance_id} , :table_alias => :instance ). - join(:enumeration_value, :id => :instance__instance_type_id). - join(:resource, { :id => :instance__resource_id }, :table_alias => :resource ). - join(:repository, :id => :resource__repo_id). - where(Sequel.qualify(:repository, :id) => @repo_id). - select(:resource__id, :resource__title, Sequel.as( :location__id, :location_id), Sequel.as( :enumeration_value__value, :instance_type)). - all - accessions = db[:location]. - join(:housed_at_rlshp, :location_id => :location__id). - join(:container, :id => :housed_at_rlshp__container_id). - join(:instance, :id => :container__instance_id). - join(:accession, :id => :instance__accession_id). - join(:repository, :id => :accession__repo_id). - where(Sequel.qualify(:repository, :id) => @repo_id). - select(:accession__id, :accession__title, :accession__identifier, Sequel.as( :location__id, :location_id)). - all - results = { :locations => locations, :resources => resources, :accessions => accessions } - end - results - " - - } - JasperReportRegister.register_report(opts) - - query = eval( "Proc.new{ #{opts["query"] } }" ) - LocationsReport.send(:define_method, :query) { query.call } - - end - - it "should have registered correctly" do - report = LocationsReport.new({:repo_id => $repo_id}, - Job.create_from_json(build(:json_job), - :repo_id => $repo_id, - :user => create_nobody_user)) - report.should be_kind_of(JSONReport) - report.should be_kind_of(JasperReport) - end - - it "can be created from a JSON module" do - - # create the record with all the instance/container etc - location = create(:json_location, :temporary => generate(:temporary_location_type)) - Resource.create_from_json( build(:json_resource, { - :instances => [build(:json_instance, { - :container => build(:json_container, { - :container_locations => [{'ref' => location.uri, - 'status' => 'current', - 'start_date' => generate(:yyyy_mm_dd), - 'end_date' => generate(:yyyy_mm_dd)}] - }) - })] - }), :repo_id => $repo_id ) - - location2 = create(:json_location, :temporary => generate(:temporary_location_type)) - Accession.create_from_json( build(:json_accession, { - :instances => [build(:json_instance, { - :container => build(:json_container, { - :container_locations => [{'ref' => location2.uri, - 'status' => 'current', - 'start_date' => generate(:yyyy_mm_dd), - 'end_date' => generate(:yyyy_mm_dd)}] - }) - })] - }), :repo_id => $repo_id ) - - # create a third useless location to make sure it doesnt show up in the - # report - create(:json_location, :temporary => generate(:temporary_location_type)) - - report = LocationsReport.new({:repo_id => $repo_id}, - Job.create_from_json(build(:json_job), - :repo_id => $repo_id, - :user => create_nobody_user)) - json = JSON( String.from_java_bytes( report.render(:json) ) ) - json["locations"].length.should == 3 - - loc1 = json["locations"][0] - loc2 = json["locations"][1] - - loc1["barcode"].should eq(location.barcode) - loc1["building"].should eq(location.building) - loc2["barcode"].should eq(location2.barcode) - loc2["building"].should eq(location2.building) - - # unsure how to test these...let's just render them and see if there are - # any errors. - #report.render(:html) - #report.render(:pdf) - #report.render(:xlsx) - end -end diff --git a/backend/spec/model_archival_object_spec.rb b/backend/spec/model_archival_object_spec.rb index 7d796329ab..c753a67642 100644 --- a/backend/spec/model_archival_object_spec.rb +++ b/backend/spec/model_archival_object_spec.rb @@ -127,18 +127,12 @@ it "enforces ref_id uniqueness only within a resource" do res1 = create(:json_resource) - res2 = create(:json_resource) create(:json_archival_object, {:ref_id => "the_same", :resource => {:ref => res1.uri}}) - create(:json_archival_object, {:ref_id => "the_same", :resource => nil}) expect { create(:json_archival_object, {:ref_id => "the_same", :resource => {:ref => res1.uri}}) }.to raise_error(JSONModel::ValidationException) - - expect { - create(:json_archival_object, {:ref_id => "the_same", :resource => nil}) - }.to_not raise_error end @@ -206,21 +200,6 @@ end - it "stores persistent_ids in the database when saving notes" do - note = build(:json_note_bibliography, - :content => ["a little note"], - :persistent_id => "something") - - obj = ArchivalObject.create_from_json(build(:json_archival_object, - 'notes' => [note])) - - NotePersistentId.filter(:persistent_id => "something", - :parent_id => obj.id, - :parent_type => 'archival_object').count.should eq(1) - end - - - it "persistent_ids are stored within the context of the tree root where applicable" do note = build(:json_note_bibliography, :content => ["a little note"], @@ -322,39 +301,4 @@ }.to_not raise_error end - - it "you can resequence children" do - resource = create(:json_resource, :id_0 => rand(1000).to_s ) - ao = ArchivalObject.create_from_json( build(:json_archival_object, :resource => {:ref => resource.uri})) - - archival_object_1 = build(:json_archival_object) - archival_object_2 = build(:json_archival_object) - - children = JSONModel(:archival_record_children).from_hash({ - "children" => [archival_object_1, archival_object_2] - }) - - - expect { - ao.add_children(children) - }.to_not raise_error - - ao = ArchivalObject.get_or_die(ao.id) - ao.children.all.length.should == 2 - # now add more! - archival_object_3 = build(:json_archival_object) - archival_object_4 = build(:json_archival_object) - - - children = JSONModel(:archival_record_children).from_hash({ - "children" => [archival_object_3, archival_object_4] - }) - ao.add_children(children) - - - expect { - ArchivalObject.resequence( ao.repo_id ) - }.to_not raise_error - - end end diff --git a/backend/spec/model_asmodel_spec.rb b/backend/spec/model_asmodel_spec.rb index b4f8c3b6bd..d99cdd8d3b 100644 --- a/backend/spec/model_asmodel_spec.rb +++ b/backend/spec/model_asmodel_spec.rb @@ -3,8 +3,10 @@ describe 'ASModel' do before(:all) do - $testdb.create_table(:asmodel_spec) do - primary_key :id + DB.open(true) do |db| + db.create_table(:asmodel_spec) do + primary_key :id + end end class TestModel < Sequel::Model(:asmodel_spec) diff --git a/backend/spec/model_classification_spec.rb b/backend/spec/model_classification_spec.rb index be136e4446..b42f33aba9 100644 --- a/backend/spec/model_classification_spec.rb +++ b/backend/spec/model_classification_spec.rb @@ -52,11 +52,6 @@ def create_classification_term(parent, properties = {}) :parent => {'ref' => term.uri}) classification.tree['children'][0]['children'][0]['title'].should eq(second_term.title) - - expect { - ClassificationTerm.resequence( classification.repo_id ) - }.to_not raise_error - end @@ -124,7 +119,7 @@ def create_classification_term(parent, properties = {}) :identifier => "id#{i}") end - terms.last.update_position_only(nil, 0) + terms.last.set_parent_and_position(terms.last.parent_id, 0) titles = classification.tree['children'].map {|e| e['title']} diff --git a/backend/spec/model_digital_object_component_spec.rb b/backend/spec/model_digital_object_component_spec.rb index 4a07244fb4..c33ac51af8 100644 --- a/backend/spec/model_digital_object_component_spec.rb +++ b/backend/spec/model_digital_object_component_spec.rb @@ -3,46 +3,13 @@ describe 'DigitalObjectComponent model' do - def create_digital_object_component - DigitalObjectComponent.create_from_json(JSONModel(:digital_object_component). - from_hash("ref_id" => SecureRandom.hex, - "component_id" => SecureRandom.hex, - "title" => "A new digital object component"), - :repo_id => $repo_id) - end - - it "Allows digital object components to be created" do - doc = create_digital_object_component + doc = create(:json_digital_object_component, + { + :title => "A new digital object component" + }) - DigitalObjectComponent[doc[:id]].title.should eq("A new digital object component") + DigitalObjectComponent[doc.id].title.should eq("A new digital object component") end - - it "you can resequence children" do - - dobj = create(:json_digital_object) - doc = DigitalObjectComponent.create_from_json( build(:json_digital_object_component, :digital_object => - { :ref => dobj.uri }) ) - - doc_child1 = build(:json_digital_object_component) - doc_child2 = build(:json_digital_object_component) - - children = JSONModel(:digital_record_children).from_hash({ - "children" => [doc_child1, doc_child2] - }) - - expect { - doc.add_children(children) - }.to_not raise_error - - doc = DigitalObjectComponent.get_or_die(doc.id) - doc.children.all.length.should == 2 - - expect { - DigitalObjectComponent.resequence( doc.repo_id ) - }.to_not raise_error - - end - end diff --git a/backend/spec/model_find_and_replace_job_spec.rb b/backend/spec/model_find_and_replace_job_spec.rb index 64e42bd89d..80a79dd078 100644 --- a/backend/spec/model_find_and_replace_job_spec.rb +++ b/backend/spec/model_find_and_replace_job_spec.rb @@ -1,10 +1,7 @@ require 'spec_helper' -require_relative '../app/lib/find_and_replace_runner' -require_relative '../app/lib/background_job_queue' def find_and_replace_job(resource_uri) json = build(:json_job, - :job_type => 'find_and_replace_job', :job => build(:json_find_and_replace_job, :find => "/foo/", :replace => "bar", @@ -45,17 +42,16 @@ def a_component(resource_uri) it "ensures that the target property exists in the target schema" do - skip("this seems to not be working when run in the suite?") resource1 = a_resource json = find_and_replace_job(resource1.uri) - json.job['property'] = "WHATEVER" + json.job['property'] = "NON-EXISTENT-PROPERTY!!!" user = create_nobody_user expect { - job = Job.create_from_json(json, - :repo_id => $repo_id, - :user => user) + Job.create_from_json(json, + :repo_id => $repo_id, + :user => user) }.to raise_error(JSONModel::ValidationException) end diff --git a/backend/spec/model_inheritance_spec.rb b/backend/spec/model_inheritance_spec.rb new file mode 100644 index 0000000000..89baf893ef --- /dev/null +++ b/backend/spec/model_inheritance_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' + +describe 'Record inheritance' do + + let(:resource) { create(:json_resource) } + + let(:parent) { + create(:json_archival_object, + :level => 'otherlevel', + :other_level => 'special', + :resource => {:ref => resource.uri}) + } + + let(:child) { + create(:json_archival_object, + :resource => {:ref => resource.uri}, + :parent => {:ref => parent.uri}) + } + + let(:grandchild) { + create(:json_archival_object, + :resource => {:ref => resource.uri}, + :parent => {:ref => child.uri}) + } + + let(:config) do + { + :archival_object => { + :composite_identifiers => { + :include_level => true, + :identifier_delimiter => '.' + }, + :inherited_fields => [ + { + :property => 'title', + :inherit_directly => true + }, + { + :property => 'component_id', + :inherit_directly => false + }, + { + :property => 'linked_agents', + :inherit_if => proc {|json| json.select {|j| j['role'] == 'subject'} }, + :inherit_directly => true + }, + { + :property => 'notes', + :inherit_if => proc {|json| json.select {|j| j['type'] == 'scopecontent'} }, + :inherit_directly => false + }, + { + :property => 'notes', + :skip_if => proc {|json| ['file', 'item'].include?(json['level']) }, + :inherit_if => proc {|json| json.select {|j| j['type'] == 'skippable'} }, + :inherit_directly => false + } + ] + } + } + end + + let(:json) do + { + 'jsonmodel_type' => 'archival_object', + 'title' => 'mine all mine', + 'component_id' => nil, + 'level' => 'file', + 'linked_agents' => [], + 'notes' => [], + 'ancestors' => [ + { + 'ref' => '/repositories/2/archival_objects/1', + 'level' => 'series', + '_resolved' => { + 'title' => 'important series title', + 'level' => 'series', + 'component_id' => 'ABC', + 'linked_agents' => [ + { 'role' => 'subject', 'name' => 'fred' }, + { 'role' => 'enemy', 'name' => 'jo' } + ], + 'notes' => [], + } + }, + { + 'ref' => '/repositories/2/resources/1', + 'level' => 'collection', + '_resolved' => { + 'title' => 'This is a resource', + 'id_0' => 'RES', + 'id_1' => '1', + 'level' => 'collection', + 'linked_agents' => [], + 'notes' => [ + {'type' => 'scopecontent', 'text' => 'Pants'}, + {'type' => 'odd', 'text' => 'Something else'}, + {'type' => 'skippable', 'text' => 'Should be skipped'} + ] + } + } + ] + } + end + + let(:record_inheritance) { RecordInheritance.new(config) } + + + it "provides refs to a record's ancestors" do + json = ArchivalObject.to_jsonmodel(grandchild.id) + + json['ancestors'].should eq([ + { + 'ref' => child.uri, + 'level' => child.level, + }, + { + 'ref' => parent.uri, + 'level' => parent.other_level, + }, + { + 'ref' => resource.uri, + 'level' => resource.level, + }, + ]) + end + + + it "configurably merges field values from ancestors into a record" do + merged = record_inheritance.merge([json]).first + + merged['title'].should eq('mine all mine') + merged['component_id'].should eq('ABC') + end + + + it "merges only directly inherited fields if asked" do + merged = record_inheritance.merge(json, :direct_only => true) + + merged['title'].should eq('mine all mine') + merged['component_id'].should be_nil + end + + + it "supports selective inheritance from array values" do + merged = record_inheritance.merge([json]).first + + merged['linked_agents'].select {|a| a['role'] == 'subject'}.should_not be_empty + merged['linked_agents'].select {|a| a['role'] == 'enemy'}.should be_empty + end + + + it "supports skipping fields" do + merged = record_inheritance.merge(json) + + merged['notes'].select {|a| a['type'] == 'skippable'}.should be_empty + end + + + it "adds inheritance properties to inherited values" do + merged = record_inheritance.merge([json]).first + + scope = merged['notes'].select {|a| a['type'] == 'scopecontent'}.first + scope['_inherited'].should_not be_nil + scope['_inherited']['ref'].should eq('/repositories/2/resources/1') + scope['_inherited']['level'].should eq('Collection') + scope['_inherited']['direct'].should be false + + subject = merged['linked_agents'].select {|a| a['role'] == 'subject'}.first + subject['_inherited']['ref'].should eq('/repositories/2/archival_objects/1') + subject['_inherited']['level'].should eq('Series') + subject['_inherited']['direct'].should be true + + merged['component_id_inherited']['ref'].should eq('/repositories/2/archival_objects/1') + merged['component_id_inherited']['level'].should eq('Series') + merged['component_id_inherited']['direct'].should be false + + merged.has_key?('title_inherited').should be false + end + + + it "adds a composite identifier" do + merged = record_inheritance.merge(json) + + merged['_composite_identifier'].should eq('RES.1. Series ABC') + end +end diff --git a/backend/spec/model_job_spec.rb b/backend/spec/model_job_spec.rb index 7a0a4012c0..04a0245ab1 100644 --- a/backend/spec/model_job_spec.rb +++ b/backend/spec/model_job_spec.rb @@ -1,25 +1,23 @@ require 'spec_helper' -describe 'job model and job runners' do +describe 'Background jobs' do before(:all) do - enum = Enumeration.find(:name => 'job_type') - EnumerationValue.create(:value => 'nugatory_job', :enumeration_id => enum.id) - BackendEnumSource.cache_entry_for('job_type', true) - - JSONModel(:job).schema['properties']['job']['type'] = 'object' + JSONModel.create_model_for("nugatory_job", + { + "$schema" => "http://www.archivesspace.org/archivesspace.json", + "version" => 1, + "type" => "object", + "properties" => {} + }) class NugatoryJobRunner < JobRunner + register_for_job_type("nugatory_job") @run_till_canceled = false - - def initialize(job) - @job = job - end - def self.run_till_canceled! @run_till_canceled = true end @@ -32,33 +30,33 @@ def self.reset @run_till_canceled = false end - - def self.instance_for(job) - if job.job_type == "nugatory_job" - self.new(job) - else - nil - end - end - - def run while self.class.run_till_canceled? - break if @job_canceled && @job_canceled.value + break if self.canceled? sleep(0.2) end end + end + + + class HiddenJobRunner < JobRunner + register_for_job_type("hidden_job", :hidden => true) + end + + class ConcurrentJobRunner < JobRunner + register_for_job_type("concurrent_job", :run_concurrently => true) + end + class PermissionJobRunner < JobRunner + register_for_job_type("permissions_job", + :create_permissions => 'god_like', + :cancel_permissions => ['death', 'destruction']) end end + after(:all) do - RequestContext.open(:repo_id => $repo_id) do - as_test_user("admin") do - EnumerationValue.filter(:value => 'nugatory_job').first.destroy - BackendEnumSource.cache_entry_for('job_type', true) - end - end + JSONModel.destroy_model(:nugatory_job) end @@ -71,23 +69,16 @@ def run let(:job) { user = create_nobody_user - - - json = JSONModel(:job).from_hash({ - :job_type => 'nugatory_job', - :job => {}, - }) - - - Job.create_from_json(json, - :repo_id => $repo_id, - :user => user) + json = JSONModel(:job).from_hash({:job => {'jsonmodel_type' => 'nugatory_job'}}) + Job.create_from_json(json, :repo_id => $repo_id, :user => user) } + it 'can get the status of a job' do job.status.should eq('queued') end + it "can get the owner of a job" do job.owner.username.should eq("nobody") end @@ -95,20 +86,17 @@ def run it "can record created URIs for a job" do job.record_created_uris((1..10).map {|n| "/repositories/#{$repo_id}/accessions/#{n}"}) - job.created_records.count.should eq(10) end it "can record modified URIs for a job" do job.record_modified_uris((1..10).map {|n| "/repositories/#{$repo_id}/accessions/#{n}"}) - job.modified_records.count.should eq(10) end it "can attach some input files to a job" do - allow(job).to receive(:file_store) do double(:store => "stored_path") end @@ -121,14 +109,63 @@ def run it 'can get the right runner for the job' do - JobRunner.for(job).class.to_s.should eq('NugatoryJobRunner') + JobRunner.for(job).class.should eq(NugatoryJobRunner) + end + + + it 'ensures only one runner can register for a job type' do + expect { + JobRunner.register_for_job_type('nugatory_job') + }.to raise_error(JobRunner::JobRunnerError) + end + + + it 'can give you the registered runner for a job type' do + runner = JobRunner.registered_runner_for('nugatory_job') + runner.type.should eq('nugatory_job') + end + + + it 'knows if a job type allows concurrency' do + JobRunner.registered_runner_for('nugatory_job').run_concurrently.should be false + JobRunner.registered_runner_for('concurrent_job').run_concurrently.should be true + end + + + it 'knows the permissions required to create or cancel a job' do + runner = JobRunner.registered_runner_for('nugatory_job') + runner.create_permissions.should eq [] + runner.cancel_permissions.should eq [] + + runner = JobRunner.registered_runner_for('permissions_job') + runner.create_permissions.should eq ['god_like'] + runner.cancel_permissions.should eq ['death', 'destruction'] + end + + + it 'can give you a list of registered job types and their permissions' do + types = JobRunner.registered_job_types + types['nugatory_job'][:create_permissions].should eq [] + end + + + it 'will not tell you about hidden job types' do + types = JobRunner.registered_job_types + types['hidden_job'].should be_nil + end + + + it 'will give you the registered runner for a hidden job type' do + runner = JobRunner.registered_runner_for('hidden_job') + runner.type.should eq('hidden_job') end it 'runs a job and keeps track of its canceled state' do - runner = JobRunner.for(job).canceled(Atomic.new(false)) + runner = JobRunner.for(job) + runner.cancelation_signaler(Atomic.new(false)) runner.run - runner.instance_variable_get(:@job_canceled).value.should == false + runner.canceled?.should == false end end @@ -139,10 +176,12 @@ def run q = BackgroundJobQueue.new } + before(:each) do @job = nil end + after(:each) do as_test_user("admin") do RequestContext.open(:repo_id => $repo_id) do @@ -156,21 +195,15 @@ def run it "can find the next queued job and start it", :skip_db_open do - json = JSONModel(:job).from_hash({ - :job_type => 'nugatory_job', - :job => {}, + :job => {'jsonmodel_type' => 'nugatory_job'}, }) as_test_user("admin") do RequestContext.open do RequestContext.put(:repo_id, $repo_id) RequestContext.put(:current_username, "admin") - - user = create(:user, :username => 'jobber') - - @job = Job.create_from_json(json, :repo_id => $repo_id, :user => user) @@ -191,16 +224,12 @@ def run it "can stop a canceled job and finish it", :skip_db_open do NugatoryJobRunner.run_till_canceled! - json = JSONModel(:job).from_hash({ - :job_type => 'nugatory_job', - :job => {}, - }) + json = JSONModel(:job).from_hash({:job => {'jsonmodel_type' => 'nugatory_job'}}) as_test_user("admin") do RequestContext.open do RequestContext.put(:repo_id, $repo_id) RequestContext.put(:current_username, "admin") - user = create(:user, :username => 'jobber') @job = Job.create_from_json(json, @@ -228,6 +257,5 @@ def run job.time_finished.should_not be_nil job.time_finished.should < Time.now end - end end diff --git a/backend/spec/model_managed_container_spec.rb b/backend/spec/model_managed_container_spec.rb index 9a96e5e003..c7a2fcd54d 100644 --- a/backend/spec/model_managed_container_spec.rb +++ b/backend/spec/model_managed_container_spec.rb @@ -331,7 +331,7 @@ def build_container_location(location_uri, status = 'current') original_mtime = top_container.refresh.system_mtime - ArchivalObject[child.id].update_position_only(grandparent.id, 1) + ArchivalObject[child.id].set_parent_and_position(grandparent.id, 1) top_container.refresh.system_mtime.should be > original_mtime end @@ -457,15 +457,14 @@ def build_container_location(location_uri, status = 'current') end it "throws exception when attempt to update to an invalid barcode" do - - stub_barcode_length(4, 6) - container1_json = create(:json_top_container) container2_json = create(:json_top_container) original_barcode_1 = TopContainer[container1_json.id].barcode original_barcode_2 = TopContainer[container2_json.id].barcode + stub_barcode_length(4, 6) + barcode_data = {} barcode_data[container1_json.uri] = "7777777" barcode_data[container2_json.uri] = "333" diff --git a/backend/spec/model_print_to_pdf_job_spec.rb b/backend/spec/model_print_to_pdf_job_spec.rb index 852105dda0..ea8cabc7f3 100644 --- a/backend/spec/model_print_to_pdf_job_spec.rb +++ b/backend/spec/model_print_to_pdf_job_spec.rb @@ -1,11 +1,7 @@ require 'spec_helper' -require_relative '../app/lib/print_to_pdf_runner' -require_relative '../app/lib/background_job_queue' - def print_to_pdf_job( resource_uri ) build( :json_job, - :job_type => 'print_to_pdf_job', :job => build(:json_print_to_pdf_job, :source => resource_uri) ) end diff --git a/backend/spec/model_relationships_spec.rb b/backend/spec/model_relationships_spec.rb index 75a13a63a1..24009d2e54 100644 --- a/backend/spec/model_relationships_spec.rb +++ b/backend/spec/model_relationships_spec.rb @@ -401,4 +401,70 @@ class Apple < Sequel::Model(:apple) end + it "updates the mtime of all related records, following nested records back to top-level records as required" do + # Ditching our fruit salad metaphor for the moment, since this actually + # happens in real life... + + # We have a digital object + digital_object = create(:json_digital_object) + + # and an archival object that links to it via instance + archival_object_json = create(:json_archival_object, + :instances => [ + build(:json_instance, + :instance_type => 'digital_object', + :digital_object => { + :ref => digital_object.uri + }) + ]) + + archival_object = ArchivalObject[archival_object_json.id] + + start_time = (archival_object[:system_mtime].to_f * 1000).to_i + sleep 0.1 + + # Touch the digital object + digital_object.refetch + digital_object.save + + # We want to see the archival object's mtime updated, since that's the + # top-level record that should be reindexed. The original bug: only the + # instance's system_mtime was updated. + archival_object.refresh + (archival_object.system_mtime.to_f * 1000).to_i.should_not eq(start_time) + end + + it "updates the mtime of all related records, following nested records back to top-level records as required" do + # Ditching our fruit salad metaphor for the moment, since this actually + # happens in real life... + + # We have a digital object + digital_object = create(:json_digital_object) + + # and an archival object that links to it via instance + archival_object_json = create(:json_archival_object, + :instances => [ + build(:json_instance, + :instance_type => 'digital_object', + :digital_object => { + :ref => digital_object.uri + }) + ]) + + archival_object = ArchivalObject[archival_object_json.id] + + start_time = (archival_object[:system_mtime].to_f * 1000).to_i + sleep 0.1 + + # Touch the digital object + digital_object.refetch + digital_object.save + + # We want to see the archival object's mtime updated, since that's the + # top-level record that should be reindexed. The original bug: only the + # instance's system_mtime was updated. + archival_object.refresh + (archival_object.system_mtime.to_f * 1000).to_i.should_not eq(start_time) + end + end diff --git a/backend/spec/model_repository_report_spec.rb b/backend/spec/model_repository_report_spec.rb index de27cf57de..ab55e92beb 100644 --- a/backend/spec/model_repository_report_spec.rb +++ b/backend/spec/model_repository_report_spec.rb @@ -1,16 +1,19 @@ require 'spec_helper' describe 'RepositoryReport model' do - it "returns a report with a repositories data" do + it "returns a report with repository data" do repo = Repository.create_from_json(JSONModel(:repository).from_hash(:repo_code => "TESTREPO", :name => "My new test repository")) + report = RepositoryReport.new({:repo_id => repo.id}, Job.create_from_json(build(:json_job), :repo_id => repo.id, - :user => create_nobody_user)) - report.to_enum.first[:repo_code].should eq(repo.repo_code) - report.to_enum.first[:name].should eq(repo.name) - + :user => create_nobody_user), + $testdb) + + report.to_enum.any? {|row| + row[:repo_code] == repo.repo_code && row[:name] == repo.name + }.should be true end end diff --git a/backend/spec/model_repository_spec.rb b/backend/spec/model_repository_spec.rb index af048f5af0..0f0a6e5752 100644 --- a/backend/spec/model_repository_spec.rb +++ b/backend/spec/model_repository_spec.rb @@ -89,7 +89,7 @@ :name => "electric boogaloo")) JSONModel.set_repository(repo.id) a_resource = create(:json_resource, { :extents => [build(:json_extent)] }) - accession = create(:json_accession, :repo_id => repo.id, + accession = create(:json_accession, :related_resources => [ {:ref => a_resource.uri } ]) dobj = create(:json_digital_object ) create(:json_digital_object_component, diff --git a/backend/spec/model_resource_spec.rb b/backend/spec/model_resource_spec.rb index 397a12e903..4ea7586cd7 100644 --- a/backend/spec/model_resource_spec.rb +++ b/backend/spec/model_resource_spec.rb @@ -152,7 +152,7 @@ expect { resource.update_from_json(json) }.to_not raise_error end - it "defaults the representative image to the first 'image-service' file_version it is linked to through it's instances" do + it "defaults the representative image to the first 'image-service' file_version it is linked to through its instances" do uris = ["http://foo.com/bar1", "http://foo.com/bar2", "http://foo.com/bar3"] do1 = create(:json_digital_object, { diff --git a/backend/spec/model_session_spec.rb b/backend/spec/model_session_spec.rb index 4f599371a0..01584e4fb6 100644 --- a/backend/spec/model_session_spec.rb +++ b/backend/spec/model_session_spec.rb @@ -31,25 +31,19 @@ end - it "knows its age" do - allow(Time).to receive(:now) { Time.at(0) } + it "becomes young again when touched" do + first_time = Time.at(0) + next_time = Time.at(10) + s = Session.new - allow(Time).to receive(:now) { Time.at(10) } - s.age.should eq(10) - end + s.touch; Session.touch_pending_sessions(first_time) + first_age = Session.find(s.id).age - it "becomes young again when touched" do - allow(Time).to receive(:now) { Time.at(0) } - s = Session.new - allow(Time).to receive(:now) { Time.at(10) } - s.touch - allow(Time).to receive(:now) { Time.at(100) } - s.age.should eq(90) - allow(Time).to receive(:now) { Time.at(110) } - s.touch - allow(Time).to receive(:now) { Time.at(111) } - s.age.should eq(1) + s.touch; Session.touch_pending_sessions(next_time) + next_age = Session.find(s.id).age + + (next_age - first_age).abs.should eq(10) end diff --git a/backend/spec/model_solr_spec.rb b/backend/spec/model_solr_spec.rb index ceb4c904f4..06ce3a1adf 100644 --- a/backend/spec/model_solr_spec.rb +++ b/backend/spec/model_solr_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'cgi' $dummy_data = <<EOF { @@ -57,7 +58,7 @@ def response.code; '200'; end query = Solr::Query.create_keyword_search("hello world"). pagination(1, 10). set_repo_id(@repo_id). - set_excluded_ids(%w(alpha omega)). + set_excluded_ids(%w(alpha omega)). set_record_types(['optional_record_type']). highlighting @@ -81,4 +82,54 @@ def response.code; '200'; end response['results'][0]['title'].should eq("A Resource") end + it "adjusts date searches for the local timezone" do + test_time = Time.parse('2000-01-01') + + advanced_query = { + "query" => { + "jsonmodel_type" => "date_field_query", + "comparator" => "equal", + "field" => "create_time", + "value" => test_time.strftime('%Y-%m-%d'), + "negated" => false + } + } + + query = Solr::Query.create_advanced_search(advanced_query) + + CGI.unescape(query.pagination(1, 10).to_solr_url.to_s).should include(test_time.utc.iso8601) + end + + + describe 'Query parsing' do + + let (:canned_query) { + {"jsonmodel_type"=>"boolean_query", + "op"=>"AND", + "subqueries"=>[{"jsonmodel_type"=>"boolean_query", + "op"=>"AND", + "subqueries"=>[{"field"=>"title", + "value"=>"Hornstein", + "negated"=>true, + "jsonmodel_type"=>"field_query", + "literal"=>false}]}, + {"jsonmodel_type"=>"boolean_query", + "op"=>"AND", + "subqueries"=>[{"jsonmodel_type"=>"boolean_query", + "op"=>"AND", + "subqueries"=>[{"field"=>"keyword", + "value"=>"*", + "negated"=>false, + "jsonmodel_type"=>"field_query", + "literal"=>false}]}]}]} + } + + it "compensates for purely negative expressions by adding a match-all clause" do + query_string = Solr::Query.construct_advanced_query_string(canned_query) + + query_string.should eq("((-title:(Hornstein) AND *:*) AND ((fullrecord:(*))))") + end + + end + end diff --git a/backend/spec/rest_helper_spec.rb b/backend/spec/rest_helper_spec.rb new file mode 100644 index 0000000000..ac4de573bb --- /dev/null +++ b/backend/spec/rest_helper_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' +require_relative '../app/lib/rest' + +describe 'Rest Helpers' do + + before(:all) do + RESTHelpers::Endpoint.get('/rest_helper_spec/get') + .description("GET endpoint test") + .permissions([]) + .returns([200, "(json)"]) \ + do + json_response(:method => env["REQUEST_METHOD"]) + end + + RESTHelpers::Endpoint.post('/rest_helper_spec/post') + .description("POST endpoint test") + .permissions([]) + .returns([200, "(json)"]) \ + do + json_response(:method => env["REQUEST_METHOD"]) + end + + RESTHelpers::Endpoint.get_or_post('/rest_helper_spec/get_or_post') + .description("GET or POST endpoint test") + .permissions([]) + .returns([200, "(json)"]) \ + do + json_response(:method => env["REQUEST_METHOD"]) + end + end + + it "can define a GET" do + get '/rest_helper_spec/get' + json = ASUtils.json_parse(last_response.body) + json['method'].should eq('GET') + end + + it "can define a POST" do + post '/rest_helper_spec/post' + json = ASUtils.json_parse(last_response.body) + json['method'].should eq('POST') + end + + it "can define both a GET and a POST" do + get '/rest_helper_spec/get_or_post' + json = ASUtils.json_parse(last_response.body) + json['method'].should eq('GET') + + post '/rest_helper_spec/get_or_post' + json = ASUtils.json_parse(last_response.body) + json['method'].should eq('POST') + end + +end diff --git a/backend/spec/spec_helper.rb b/backend/spec/spec_helper.rb index f281a6f3be..299ced7436 100644 --- a/backend/spec/spec_helper.rb +++ b/backend/spec/spec_helper.rb @@ -23,41 +23,56 @@ # Use an in-memory Derby DB for the test suite class DB - def self.connect - if not @pool - require "db/db_migrator" - if ENV['ASPACE_TEST_DB_URL'] - test_db_url = ENV['ASPACE_TEST_DB_URL'] - else - test_db_url = "jdbc:derby:memory:fakedb;create=true" + def self.get_default_pool + @default_pool + end + + class DBPool + + def connect + # If we're not connected, we're in the process of setting up the primary + # DB pool, so go ahead and connect to an in-memory Derby instance. + if DB.get_default_pool == :not_connected + require "db/db_migrator" - begin - java.lang.Class.for_name("org.h2.Driver") - test_db_url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" - rescue java.lang.ClassNotFoundException - # Oh well. Derby it is! + if ENV['ASPACE_TEST_DB_URL'] + test_db_url = ENV['ASPACE_TEST_DB_URL'] + else + test_db_url = "jdbc:derby:memory:fakedb;create=true" + + begin + java.lang.Class.for_name("org.h2.Driver") + test_db_url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" + rescue java.lang.ClassNotFoundException + # Oh well. Derby it is! + end end - end - @pool = Sequel.connect(test_db_url, - :max_connections => 10, - #:loggers => [Logger.new($stderr)] - ) + @pool = Sequel.connect(test_db_url, + :max_connections => 10, + #:loggers => [Logger.new($stderr)] + ) - unless ENV['ASPACE_TEST_DB_PERSIST'] - DBMigrator.nuke_database(@pool) - end + unless ENV['ASPACE_TEST_DB_PERSIST'] + DBMigrator.nuke_database(@pool) + end - DBMigrator.setup_database(@pool) + DBMigrator.setup_database(@pool) + + self + else + # For the sake of our tests, have all pools share the same Derby. + DB.get_default_pool + end end - end + # For the sake of unit tests, just fire these straight away (since the entire + # test always runs in a transaction) + def after_commit(&block) + block.call + end - # For the sake of unit tests, just fire these straight away (since the entire - # test always runs in a transaction) - def self.after_commit(&block) - block.call end end diff --git a/backend/spec/tree_position_spec.rb b/backend/spec/tree_position_spec.rb index e860d9979b..a3a5350963 100644 --- a/backend/spec/tree_position_spec.rb +++ b/backend/spec/tree_position_spec.rb @@ -96,11 +96,11 @@ def refresh! # # However, things go wrong when we take this position, load it into a # JSONModel and expose it through the API. The API and frontend work with - # absolute positions, where setting a record to position = 2 always means + # logical positions, where setting a record to position = 2 always means # "this is the third item in the list". If the frontend takes the second # record (with a position of '5' in the DB) and updates it, it passes # 'position = 5' back through to the backend. The backend then assumes this - # '5' is an absolute '5', so it adjusts it to be relative to the other + # '5' is an logical '5', so it adjusts it to be relative to the other # numbers it has. Since it appears that the frontend wanted the second # record to be moved to position 5, and since there are only three records, # it moves the record to the end of the list. diff --git a/backend/tests/integration.rb b/backend/tests/integration.rb index 40ca034728..1212e4d59c 100755 --- a/backend/tests/integration.rb +++ b/backend/tests/integration.rb @@ -24,10 +24,11 @@ def url(uri) end -def do_post(s, url) +def do_post(s, url, content_type = 'application/x-www-form-urlencoded') Net::HTTP.start(url.host, url.port) do |http| req = Net::HTTP::Post.new(url.request_uri) req.body = s + req['Content-Type'] = content_type req["X-ARCHIVESSPACE-SESSION"] = @session if @session r = http.request(req) @@ -84,7 +85,8 @@ def run_tests(opts) puts "Create a test user" r = do_post({:username => test_user, :name => test_user}.to_json, - url("/users?password=testuser")) + url("/users?password=testuser"), + 'text/json') r[:body]['status'] == 'Created' or fail("Test user creation", r) @@ -109,7 +111,8 @@ def run_tests(opts) :name => "Test #{$me}", :description => "integration test repository #{$$}" }.to_json, - url("/repositories")) + url("/repositories"), + 'text/json') repo_id = r[:body]["id"] or fail("Repository creation", r) @@ -120,7 +123,8 @@ def run_tests(opts) :name => "Another Test #{$me}", :description => "another integration test repository #{$$}" }.to_json, - url("/repositories")) + url("/repositories"), + 'text/json') second_repo_id = r[:body]["id"] or fail("Second repository creation", r) @@ -131,7 +135,8 @@ def run_tests(opts) :title => "integration test accession #{$$}", :accession_date => "2011-01-01" }.to_json, - url("/repositories/#{repo_id}/accessions")) + url("/repositories/#{repo_id}/accessions"), + 'text/json') acc_id = r[:body]["id"] or fail("Accession creation", r) @@ -151,7 +156,8 @@ def run_tests(opts) :external_ids => [{'source' => 'mark', 'external_id' => 'rhubarb'}], :accession_date => "2011-01-01" }.to_json, - url("/repositories/#{second_repo_id}/accessions")) + url("/repositories/#{second_repo_id}/accessions"), + 'text/json') r[:body]["id"] or fail("Second accession creation", r) @@ -162,7 +168,8 @@ def run_tests(opts) :terms => [], :vocabulary => "/vocabularies/1" }.to_json, - url("/subjects")) + url("/subjects"), + 'text/json') r[:status] === "400" or fail("Invalid subject check", r) @@ -176,7 +183,8 @@ def run_tests(opts) ], :vocabulary => "/vocabularies/1" }.to_json, - url("/subjects")) + url("/subjects"), + 'text/json') subject_id = r[:body]["id"] or fail("Subject creation", r) @@ -191,7 +199,8 @@ def run_tests(opts) :level => "collection", :extents => [{"portion" => "whole", "number" => "5 or so", "extent_type" => "reels"}] }.to_json, - url("/repositories/#{repo_id}/resources")) + url("/repositories/#{repo_id}/resources"), + 'text/json') coll_id = r[:body]["id"] or fail("Resource creation", r) @@ -204,31 +213,12 @@ def run_tests(opts) :resource => {'ref' => "/repositories/#{repo_id}/resources/#{coll_id}"}, :level => "item" }.to_json, - url("/repositories/#{repo_id}/archival_objects")) + url("/repositories/#{repo_id}/archival_objects"), + 'text/json') ao_id = r[:body]["id"] or fail("Archival Object creation", r) - puts "Create a standalone archival object" - r = do_post({ - :ref_id => "test#{$me}", - :title => "integration test archival object #{$$} - standalone", - :subjects => [{"ref" => "/subjects/#{subject_id}"}], - :level => "item" - }.to_json, - url("/repositories/#{repo_id}/archival_objects")) - - standalone_ao_id = r[:body]["id"] or fail("Standalone Archival Object creation", r) - - - puts "Retrieve the archival object with subjects resolved" - r = do_get(url("/repositories/#{repo_id}/archival_objects/#{ao_id}?resolve[]=subjects")) - r[:body]["subjects"][0]["_resolved"]["terms"][0]["term"] == "Some term #{$me}" or - fail("Archival object fetch", r) - - - - puts "Catch reference errors in batch imports" r = do_post([{ :jsonmodel_type => "resource", @@ -240,7 +230,8 @@ def run_tests(opts) :level => "collection", :extents => [{"portion" => "whole", "number" => "5 or so", "extent_type" => "reels"}] }].to_json, - url("/repositories/#{repo_id}/batch_imports")) + url("/repositories/#{repo_id}/batch_imports"), + 'text/json') r[:body].last["errors"] or fail("Catch reference errors", r) @@ -255,7 +246,8 @@ def run_tests(opts) :level => "collection", :extents => [{"portion" => "whole", "number" => "5 or so", "extent_type" => "reels"}] }].to_json, - url("/repositories/#{repo_id}/batch_imports")) + url("/repositories/#{repo_id}/batch_imports"), + 'text/json') r[:body].last["saved"] or fail("Rollback reference errors", r) diff --git a/build/build.xml b/build/build.xml index 94f2a5a569..4fbeb87f95 100644 --- a/build/build.xml +++ b/build/build.xml @@ -2,8 +2,8 @@ <project name="ArchivesSpace" default="help"> - <property name="jruby_url" value="http://jruby.org.s3.amazonaws.com/downloads/1.7.22/jruby-complete-1.7.22.jar" /> - <property name="jruby_file" value="jruby-complete-1.7.22.jar" /> + <property name="jruby_url" value="https://s3.amazonaws.com/jruby.org/downloads/9.1.8.0/jruby-complete-9.1.8.0.jar" /> + <property name="jruby_file" value="jruby-complete-9.1.8.0.jar" /> <property name="solr_url" value="http://repo1.maven.org/maven2/org/apache/solr/solr/4.10.4/solr-4.10.4.war" /> <property name="solr_file" value="solr-4.10.4.war" /> @@ -19,8 +19,11 @@ <property name="aspace.data_directory" value="${basedir}/../build" /> <property environment="env"/> - <property name="env.JAVA_OPTS" value="-XX:MaxPermSize=196m -Xmx300m -Xss2m" /> - <property name="default_java_options" value="-Daspace.config.data_directory=${aspace.data_directory} -Dfile.encoding=UTF-8 -Daspace.config.search_user_secret=devserver -Daspace.config.public_user_secret=devserver -Daspace.config.staff_user_secret=devserver -Daspace.devserver=true -Daspace.config.frontend_cookie_secret=devserver -Daspace.config.public_cookie_secret=devserver -Daspace.config.solr_url=http://localhost:${aspace.solr.port}" /> + <!-- + Extra options for people who like lots of GC detail: -verbose:gc -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime + --> + <property name="env.JAVA_OPTS" value="" /> + <property name="default_java_options" value="-XX:MaxPermSize=196m -Djava.security.egd=file:/dev/./urandom -Xmx600m -Xss2m -Daspace.config.data_directory=${aspace.data_directory} -Dfile.encoding=UTF-8 -Daspace.config.search_user_secret=devserver -Daspace.config.public_user_secret=devserver -Daspace.config.staff_user_secret=devserver -Daspace.devserver=true -Daspace.config.frontend_cookie_secret=devserver -Daspace.config.public_cookie_secret=devserver -Daspace.config.solr_url=http://localhost:${aspace.solr.port}" /> <target name="help" description="This help"> @@ -76,7 +79,7 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="HOME" value="${build.home}" /> - <arg line="--1.9 gems/bin/bundle install --gemfile='${gemfile}' --no-deployment --without '${excluded-gem-groups}'" /> + <arg line="gems/bin/bundle install --gemfile='${gemfile}' --no-deployment --without '${excluded-gem-groups}'" /> </java> </target> @@ -87,7 +90,7 @@ <property name="excluded-gem-groups" value="" /> <property name="build.home" location="."/> <condition property="public-dir" value="public-new" else="public"> - <equals arg1="${env.ASPACE_PUBLIC_DEV}" arg2="true" /> + <equals arg1="${env.ASPACE_PUBLIC_NEW}" arg2="true" /> </condition> <java classpath="${jruby_file}" classname="org.jruby.Main" fork="true" failonerror="true"> @@ -96,7 +99,7 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="HOME" value="${build.home}" /> - <arg line="--1.9 -S gem install bundler -v 1.12.5" /> + <arg line="-S gem install bundler -v 1.12.5" /> </java> <antcall target="bundler"> @@ -120,7 +123,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="-Iapp/lib --1.9 build/scripts/migrate_db.rb" /> + <arg line="-Iapp/lib build/scripts/migrate_db.rb" /> </java> </target> @@ -134,7 +137,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="-Iapp/lib --1.9 build/scripts/migrate_db.rb nuke" /> + <arg line="-Iapp/lib build/scripts/migrate_db.rb nuke" /> </java> </target> @@ -149,7 +152,14 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="COVERAGE_REPORTS" value="${COVERAGE_REPORTS}" /> - <arg line="--1.9 ../build/gems/bin/rspec -P '*_spec.rb' --order rand:1 spec" /> + + <!-- NOTE: We explicitly add '.' to the load path here because + we're running from common/ and JRuby 9k refuses to load + `config/config-distribution` because it won't load anything + from the current directory on principle (even when that current + directory is on the classpath!). Explicitly setting the load + path fixes this test. --> + <arg line="-I. ../build/gems/bin/rspec -P '*_spec.rb' --order rand:1 spec" /> </java> </target> @@ -176,7 +186,7 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="COVERAGE_REPORTS" value="${COVERAGE_REPORTS}" /> - <arg line="--1.9 --debug -X-C ../build/gems/bin/rspec -b --format d -P '*_spec.rb' --order rand:1 ${example-arg} spec/${spec} ${plugin-spec-dirs}" /> + <arg line="../build/gems/bin/rspec -b --format d -P '*_spec.rb' --order rand:1 ${example-arg} spec/${spec} ${plugin-spec-dirs}" /> </java> </target> @@ -220,7 +230,7 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="COVERAGE_REPORTS" value="${COVERAGE_REPORTS}" /> - <arg line="--1.9 tests/integration.rb" /> + <arg line="tests/integration.rb" /> </java> </target> @@ -232,7 +242,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 ../build/gems/bin/warble war" /> + <arg line="../build/gems/bin/warble war" /> </java> </target> @@ -244,7 +254,7 @@ <parallel> <daemons> <java - classpath="${winstone_file}:../solr:../common/lib/slf4j-api-1.7.17.jar:../common/lib/jcl-over-slf4j-1.7.17.jar" + classpath="${winstone_file}:../solr:../common/lib/*" classname="winstone.Launcher" fork="true"> <jvmarg line="${solr.properties} -Djava.io.tmpdir=${java.io.tmpdir}/winstone.${user.name}" /> <arg line="--warfile=${solr_file} --httpPort=${aspace.solr.port} --ajp13Port=-1" /> @@ -253,12 +263,12 @@ <record name="backend_test_log.out" action="start" /> <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" failonerror="true" dir="../backend"> - <jvmarg line="-Daspace.config.backend_url=http://localhost:${aspace.backend.port} -Daspace.config.ignore_schema_info_check=true ${default_java_options} ${env.JAVA_OPTS} ${solr.properties}"/> + <jvmarg line="-Daspace.service=backend -Daspace.config.backend_url=http://localhost:${aspace.backend.port} -Daspace.config.ignore_schema_info_check=true ${default_java_options} ${env.JAVA_OPTS} ${solr.properties}"/> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="ASPACE_INTEGRATION" value="${aspace.integration}" /> - <arg line="--1.9 app/main.rb ${aspace.backend.port}" /> + <arg line="app/main.rb ${aspace.backend.port}" /> </java> <record name="backend_test_log.out" action="stop" /> </parallel> @@ -273,7 +283,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 -Iapp scripts/endpoint_doc.rb"/> + <arg line="-Iapp scripts/endpoint_doc.rb"/> <arg value="${match}"/> </java> </target> @@ -283,11 +293,11 @@ <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" failonerror="true" dir="../indexer"> - <jvmarg line="-Daspace.config.backend_url=http://localhost:${aspace.backend.port}/ ${default_java_options} ${env.JAVA_OPTS}"/> + <jvmarg line="-Daspace.service=indexer -Daspace.config.backend_url=http://localhost:${aspace.backend.port}/ ${default_java_options} ${env.JAVA_OPTS}"/> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 app/main.rb" /> + <arg line="app/main.rb" /> </java> </target> @@ -299,7 +309,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 ../build/gems/bin/warble war" /> + <arg line="../build/gems/bin/warble war" /> </java> </target> @@ -329,7 +339,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 --debug -X-C ../build/gems/bin/rspec -b --format d -P '*_spec.rb' --order rand:1 ${example-arg} spec/${spec}" /> + <arg line="../build/gems/bin/rspec -b --format d -P '*_spec.rb' --order rand:1 ${example-arg} spec/${spec}" /> </java> </target> @@ -364,15 +374,20 @@ <target name="frontend:devserver" depends="set-classpath, frontend:clean" description="Start an instance of the ArchivesSpace frontend development server"> + <condition property="rails.env" value="test" else="development"> + <matches pattern="true" string="${aspace.integration}" /> + </condition> + <record name="frontend_test_log.out" action="start" /> <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" failonerror="true" dir="../frontend"> - <jvmarg line="-Daspace.config.backend_url=http://localhost:${aspace.backend.port} ${default_java_options} ${env.JAVA_OPTS}"/> + <jvmarg line="-Daspace.service=frontend -Daspace.config.backend_url=http://localhost:${aspace.backend.port} ${default_java_options} ${env.JAVA_OPTS}"/> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> + <env key="RAILS_ENV" value="${rails.env}" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="ASPACE_INTEGRATION" value="${aspace.integration}" /> - <arg line="--1.9 script/rails s Puma --port=${aspace.frontend.port}" /> + <arg line="script/rails s mizuno -b 0.0.0.0 --port=${aspace.frontend.port}" /> </java> <record name="frontend_test_log.out" action="stop" /> </target> @@ -385,7 +400,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 script/rails console" /> + <arg line="script/rails console" /> </java> </target> @@ -399,10 +414,11 @@ failonerror="true" dir="../frontend"> <jvmarg line="${default_java_options} ${env.JAVA_OPTS}" /> + <env key="RAILS_ENV" value="production" /> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 -S rake assets:precompile --trace" /> + <arg line="-S rake assets:precompile --trace" /> </java> <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" @@ -412,7 +428,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 ../build/gems/bin/warble war" /> + <arg line="../build/gems/bin/warble war" /> </java> </target> @@ -438,7 +454,7 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="COVERAGE_REPORTS" value="${COVERAGE_REPORTS}" /> - <arg line="--1.9 ../build/gems/bin/rspec -P '*_spec.rb' --order default -f d ${example-arg} spec/${spec} ${plugin-selenium-dirs}" /> + <arg line="../build/gems/bin/rspec -P '*_spec.rb' --order default -f d ${example-arg} spec/${spec} ${plugin-selenium-dirs}" /> </java> </target> @@ -489,7 +505,7 @@ <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="COVERAGE_REPORTS" value="${COVERAGE_REPORTS}" /> - <arg line="--1.9 ../build/gems/bin/rspec -P '*_spec.rb' --order default -f d ${example-arg} spec/${spec} ${plugin-selenium-dirs}" /> + <arg line="../build/gems/bin/rspec -P '*_spec.rb' --order default -f d ${example-arg} spec/${spec} ${plugin-selenium-dirs}" /> </java> </target> @@ -508,7 +524,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 -S rake integration:staff cores=${cores} ${only-group-arg}" /> + <arg line="-S rake integration:staff cores=${cores} ${only-group-arg}" /> </java> </target> @@ -521,7 +537,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 -S rake doc:gen" /> + <arg line="-S rake doc:gen" /> </java> </target> @@ -534,7 +550,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 build/gems/bin/yardoc" /> + <arg line="build/gems/bin/yardoc" /> </java> <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" @@ -544,7 +560,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 build/gems/bin/yardoc -f txt" /> + <arg line="build/gems/bin/yardoc -f txt" /> </java> </target> @@ -556,7 +572,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 -Iapp scripts/build_docs.rb"/> + <arg line="-Iapp scripts/build_docs.rb"/> <arg value="${match}"/> </java> </target> @@ -620,7 +636,7 @@ <delete failonerror="false" dir="target" /> <condition property="public-dir" value="public-new" else="public"> - <equals arg1="${env.ASPACE_PUBLIC_DEV}" arg2="true" /> + <equals arg1="${env.ASPACE_PUBLIC_NEW}" arg2="true" /> </condition> <mkdir dir="target/archivesspace/config" /> @@ -729,7 +745,7 @@ <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="-Iapp/lib --1.9 scripts/export_config.rb target/archivesspace/config/config.rb" /> + <arg line="-Iapp/lib scripts/export_config.rb target/archivesspace/config/config.rb" /> </java> <zip zipfile="../archivesspace-${version}.zip" level="9"> @@ -795,7 +811,7 @@ <!-- Public Interface --> <target name="public:clean" description="Delete the Rails tmp directory"> <condition property="public-dir" value="public-new" else="public"> - <equals arg1="${env.ASPACE_PUBLIC_DEV}" arg2="true" /> + <equals arg1="${env.ASPACE_PUBLIC_NEW}" arg2="true" /> </condition> <delete dir="../${public-dir}/tmp" /> <delete dir="../${public-dir}/public/assets" /> @@ -804,28 +820,55 @@ </target> - <target name="public:devserver" depends="set-classpath, public:clean" description="Start an instance of the ArchivesSpacePublic development server"> + <target name="public:copy-shared-resources" description="Copy shared JS/CSS into the public application"> + <antcall target="-public:copy-shared-resources"><param name="publicdir" value="public-new" /></antcall> + <antcall target="-public:copy-shared-resources"><param name="publicdir" value="public" /></antcall> + + </target> + + <target name="-public:copy-shared-resources" description="Copy shared JS/CSS into the public application"> + <property name="publicdir" value="" /> + + <mkdir dir="../${publicdir}/vendor/assets/stylesheets/archivesspace" /> + + <concat destfile="../${publicdir}/vendor/assets/javascripts/largetree.js.erb"> + <header trimleading="yes"><![CDATA[<%# DON'T EDIT THIS FILE -- the canonical version is under the frontend project %> + ]]> + </header> + <fileset file="../frontend/app/assets/javascripts/largetree.js.erb" /> + </concat> + <concat destfile="../${publicdir}/vendor/assets/stylesheets/archivesspace/largetree.scss"> + <header trimleading="yes"> + // DON'T EDIT THIS FILE -- the canonical version is under the frontend project + </header> + <fileset file="../frontend/app/assets/stylesheets/archivesspace/largetree.less" /> + </concat> + + </target> + + + <target name="public:devserver" depends="set-classpath, public:clean, public:copy-shared-resources" description="Start an instance of the ArchivesSpacePublic development server"> <condition property="public-dir" value="public-new" else="public"> - <equals arg1="${env.ASPACE_PUBLIC_DEV}" arg2="true" /> + <equals arg1="${env.ASPACE_PUBLIC_NEW}" arg2="true" /> </condition> <condition property="rails-bin" value="bin" else="script"> - <equals arg1="${env.ASPACE_PUBLIC_DEV}" arg2="true" /> + <equals arg1="${env.ASPACE_PUBLIC_NEW}" arg2="true" /> </condition> <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" failonerror="true" dir="../${public-dir}"> - <jvmarg line="-Daspace.config.backend_url=http://localhost:${aspace.backend.port}/ ${default_java_options} ${env.JAVA_OPTS}"/> + <jvmarg line="-Daspace.service=public -Daspace.config.backend_url=http://localhost:${aspace.backend.port}/ ${default_java_options} ${env.JAVA_OPTS}"/> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> <env key="ASPACE_INTEGRATION" value="${aspace.integration}" /> - <arg line="--1.9 ${rails-bin}/rails s Puma --port=${aspace.public.port}" /> + <arg line="${rails-bin}/rails s mizuno -b 0.0.0.0 --port=${aspace.public.port}" /> </java> </target> - <target name="public:war" depends="set-classpath, public:clean" description="Deploy the public application as a .war file"> + <target name="public:war" depends="set-classpath, public:clean, public:copy-shared-resources" description="Deploy the public application as a .war file"> <condition property="public-dir" value="public-new" else="public"> - <equals arg1="${env.ASPACE_PUBLIC_DEV}" arg2="true" /> + <equals arg1="${env.ASPACE_PUBLIC_NEW}" arg2="true" /> </condition> <echo message="Precompiling Rails assets for Public (this can take a little while...)" /> @@ -835,20 +878,22 @@ failonerror="true" dir="../${public-dir}"> <jvmarg line="${default_java_options} ${env.JAVA_OPTS}" /> + <env key="RAILS_ENV" value="production" /> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 -S rake assets:precompile --trace" /> + <arg line="-S rake assets:precompile --trace" /> </java> <java classpath="${jruby_classpath}" classname="org.jruby.Main" fork="true" failonerror="true" dir="../${public-dir}"> <jvmarg line="${default_java_options} ${env.JAVA_OPTS}"/> + <env key="RAILS_ENV" value="production" /> <env key="GEM_HOME" value="${gem_home}" /> <env key="GEM_PATH" value="" /> <env key="BUNDLE_PATH" value="${gem_home}" /> - <arg line="--1.9 ../build/gems/bin/warble war" /> + <arg line="../build/gems/bin/warble war" /> </java> </target> diff --git a/build/scripts/migrate_db.rb b/build/scripts/migrate_db.rb index a56ea9176a..c2cd0894e3 100644 --- a/build/scripts/migrate_db.rb +++ b/build/scripts/migrate_db.rb @@ -14,7 +14,7 @@ if (AppConfig[:db_url] =~ /jdbc:derby:(.*?);.*aspacedemo=true$/) dir = $1 - if File.directory?(dir) and File.exists?(File.join(dir, "seg0")) + if File.directory?(dir) and File.exist?(File.join(dir, "seg0")) puts "Nuking demo database: #{dir}" sleep(5) FileUtils.rm_rf(dir) @@ -33,7 +33,7 @@ DBMigrator.nuke_database(db) indexer_state = File.join(AppConfig[:data_directory], "indexer_state") - if Dir.exists? (indexer_state) + if Dir.exist? (indexer_state) FileUtils.rm_rf(indexer_state) end diff --git a/clustering/files/archivesspace/config/tenant.rb b/clustering/files/archivesspace/config/tenant.rb index b1395384f1..3905ad24b1 100644 --- a/clustering/files/archivesspace/config/tenant.rb +++ b/clustering/files/archivesspace/config/tenant.rb @@ -4,7 +4,7 @@ load File.join(File.dirname(__FILE__), "config.rb") host_config = File.join($basedir, "instance_#{$hostname}.rb") -if File.exists?(host_config) +if File.exist?(host_config) config = eval(File.read(host_config)) config.each do |setting, value| AppConfig[setting] = value diff --git a/common/advanced_query_builder.rb b/common/advanced_query_builder.rb index e5f1299494..5ac40e611a 100644 --- a/common/advanced_query_builder.rb +++ b/common/advanced_query_builder.rb @@ -1,59 +1,140 @@ class AdvancedQueryBuilder - def initialize(queries, visibility) - @queries = queries - @visibility = visibility + attr_reader :query - raise "Invalid visibility value: #{visibility}" unless [:staff, :public].include?(visibility) - validate_queries! + def initialize + @query = nil end + def and(field_or_subquery, value = nil, type = 'text', literal = false, negated = false) + if field_or_subquery.is_a?(AdvancedQueryBuilder) + push_subquery('AND', field_or_subquery) + else + raise "Missing value" unless value + push_term('AND', field_or_subquery, value, type, literal, negated) + end + + self + end + + def or(field_or_subquery, value = nil, type = 'text', literal = false, negated = false) + if field_or_subquery.is_a?(AdvancedQueryBuilder) + push_subquery('OR', field_or_subquery) + else + raise "Missing value" unless value + push_term('OR', field_or_subquery, value, type, literal, negated) + end + + self + end + + def empty? + @query.nil? + end + + def build + JSONModel::JSONModel(:advanced_query).from_hash({"query" => build_query(@query)}) + end + + def self.from_json_filter_terms(array_of_json) + builder = new + + array_of_json.each do |json_str| + json = ASUtils.json_parse(json_str) + builder.and(json.keys[0], json.values[0]) + end + + builder.build + end + + def self.build_query_from_form(queries) - def build_query - query = if @queries.length > 1 - stack = @queries.reverse.clone + query = if queries.length > 1 + stack = queries.reverse.clone while stack.length > 1 a = stack.pop b = stack.pop - stack.push(JSONModel(:boolean_query).from_hash({ + stack.push(JSONModel::JSONModel(:boolean_query).from_hash({ :op => b["op"], - :subqueries => [as_subquery(a), as_subquery(b)] + :subqueries => [as_field_query(a), as_field_query(b)] })) end stack.pop else - as_subquery(@queries[0]) + as_field_query(queries[0]) end - JSONModel(:advanced_query).from_hash({"query" => query}) + JSONModel::JSONModel(:advanced_query).from_hash({"query" => query}) end private - def validate_queries! - @queries.each do |query| - invalid = AdvancedSearch.fields_matching(:name => query['field'], - :visibility => @visibility, - :type => query['type']).empty? - raise "Invalid query: #{query.inspect}" if invalid + def push_subquery(operator, subquery) + new_query = { + 'operator' => operator, + 'type' => 'boolean_query', + 'arg1' => subquery.query, + 'arg2' => @query, + } + + @query = new_query + end + + def push_term(operator, field, value, type = 'text', literal = false, negated = false) + new_query = { + 'operator' => operator, + 'type' => 'boolean_query', + 'arg1' => { + 'field' => field, + 'value' => value, + 'type' => type, + 'negated' => negated, + 'literal' => literal, + }, + 'arg2' => @query, + } + + @query = new_query + end + + def build_query(query) + if query['type'] == 'boolean_query' + subqueries = [query['arg1'], query['arg2']].compact.map {|subquery| + build_query(subquery) + } + + JSONModel::JSONModel(:boolean_query).from_hash({ + 'op' => query['operator'], + 'subqueries' => subqueries + }) + else + self.class.as_field_query(query) end end - def as_subquery(query_data) - if query_data.kind_of? JSONModelType + def self.as_field_query(query_data) + raise "keys should be strings only" if query_data.kind_of?(Hash) && query_data.any?{ |k,_| k.is_a? Symbol } + + if query_data.kind_of?(JSONModelType) query_data - elsif query_data["type"] == "date" - JSONModel(:date_field_query).from_hash(query_data) - elsif query_data["type"] == "boolean" - JSONModel(:boolean_field_query).from_hash(query_data) + elsif query_data['type'] == "date" + JSONModel::JSONModel(:date_field_query).from_hash(query_data) + elsif query_data['type'] == "boolean" + JSONModel::JSONModel(:boolean_field_query).from_hash(query_data) + elsif query_data['type'] == "range" + JSONModel::JSONModel(:range_query).from_hash(query_data) else - query = JSONModel(:field_query).from_hash(query_data) + if query_data["type"] == "enum" && query_data["value"].blank? + query_data["comparator"] = "empty" + end + + query = JSONModel::JSONModel(:field_query).from_hash(query_data) - if query_data["type"] == "enum" + if query_data['type'] == "enum" query.literal = true end diff --git a/common/advanced_search.rb b/common/advanced_search.rb index 3054de60ef..8a366e8a1c 100644 --- a/common/advanced_search.rb +++ b/common/advanced_search.rb @@ -18,7 +18,7 @@ def self.fields_matching(query) def self.solr_field_for(field) load_definitions field = @fields.fetch(field.to_s) do - raise "Unrecognized search field: #{field}" + return field end field.solr_field @@ -30,7 +30,7 @@ def self.load_definitions require 'search_definitions' ASUtils.find_local_directories("search_definitions.rb").each do |file| - if File.exists?(file) + if File.exist?(file) load File.absolute_path(file) end end diff --git a/common/aspace_gems.rb b/common/aspace_gems.rb index 5fa6f9838c..a696663ce8 100644 --- a/common/aspace_gems.rb +++ b/common/aspace_gems.rb @@ -30,7 +30,7 @@ def self.setup ASUtils.find_local_directories.each do |plugin| gemdir = File.join(plugin, "gems") - if gemdir && Dir.exists?(gemdir) + if gemdir && Dir.exist?(gemdir) gem_paths << gemdir end end diff --git a/common/aspace_i18n.rb b/common/aspace_i18n.rb index e67ad51318..40fbc1d625 100644 --- a/common/aspace_i18n.rb +++ b/common/aspace_i18n.rb @@ -8,10 +8,13 @@ I18n.load_path += ASUtils.find_locales_directories(File.join("enums", "#{AppConfig[:locale]}.yml")) # Allow overriding of the i18n locales via the 'local' folder(s) -ASUtils.wrap(ASUtils.find_local_directories).map{|local_dir| File.join(local_dir, 'frontend', 'locales')}.reject { |dir| !Dir.exists?(dir) }.each do |locales_override_directory| +ASUtils.wrap(ASUtils.find_local_directories).map{|local_dir| File.join(local_dir, 'frontend', 'locales')}.reject { |dir| !Dir.exist?(dir) }.each do |locales_override_directory| I18n.load_path += Dir[File.join(locales_override_directory, '**' , '*.{rb,yml}')] end +# Add report i18n locales +I18n.load_path += Dir[File.join(ASUtils.find_base_directory, 'reports', '**', '*.yml')] + module I18n diff --git a/common/asutils.rb b/common/asutils.rb index c42ef98220..b6b05a1efe 100644 --- a/common/asutils.rb +++ b/common/asutils.rb @@ -1,5 +1,10 @@ +# Note: ASUtils gets pulled in all over the place, and in some places prior to +# any gems having been loaded. Be careful about loading gems here, as the gem +# path might not yet be configured. For example, loading the 'json' gem can +# cause you to pull in the version that ships with JRuby, rather than the one in +# your Gemfile. + require 'java' -require 'json' require 'tmpdir' require 'tempfile' require 'config/config-distribution' @@ -79,11 +84,18 @@ def self.to_json(obj, opts = {}) def self.find_base_directory(root = nil) - [java.lang.System.get_property("ASPACE_LAUNCHER_BASE"), + # JRuby 9K seems to be adding this strange suffix... + # + # Example: /pat/to/archivesspace/backend/uri:classloader: + this_dir = __dir__.gsub(/uri:classloader:\z/, '') + + res = [java.lang.System.get_property("ASPACE_LAUNCHER_BASE"), java.lang.System.get_property("catalina.base"), - File.join(*[File.dirname(__FILE__), "..", root].compact)].find {|dir| - dir && Dir.exists?(dir) + File.join(*[this_dir, "..", root].compact)].find {|dir| + dir && Dir.exist?(dir) } + + res end @@ -166,7 +178,7 @@ def self.deep_merge(hash1, hash2) def self.load_plugin_gems(context) ASUtils.find_local_directories.each do |plugin| gemfile = File.join(plugin, 'Gemfile') - if File.exists?(gemfile) + if File.exist?(gemfile) context.instance_eval(File.read(gemfile)) end end @@ -184,14 +196,28 @@ def self.wrap(object) end end - # find a nested key inside a hash - def self.search_nested(hash,key) - if hash.respond_to?(:key?) && hash.key?(key) - hash[key] - elsif hash.respond_to?(:each) - obj = nil - hash.find{ |*a| obj=self.search_nested(a.last,key) } - obj + # Recursively find any hash entry whose key is in `keys`. When we find a + # match, call `block` with the key and value as arguments. + # + # Skips descending into any hash entry whose key is in `ignore_keys` (allowing + # us to avoid walking '_resolved' subtrees, for example) + def self.search_nested(elt, keys, ignore_keys = [], &block) + if elt.respond_to?(:key?) + keys.each do |key| + if elt.key?(key) + block.call(key, elt.fetch(key)) + end + end + + elt.each.each do |next_key, value| + unless ignore_keys.include?(next_key) + search_nested(value, keys, ignore_keys, &block) + end + end + elsif elt.respond_to?(:each) + elt.each do |value| + search_nested(value, keys, ignore_keys, &block) + end end end diff --git a/common/config/config-defaults.rb b/common/config/config-defaults.rb index 958b2b4ed6..4697548748 100644 --- a/common/config/config-defaults.rb +++ b/common/config/config-defaults.rb @@ -28,6 +28,12 @@ AppConfig[:indexer_thread_count] = 4 AppConfig[:indexer_solr_timeout_seconds] = 300 +# PUI Indexer Settings +AppConfig[:pui_indexer_enabled] = true +AppConfig[:pui_indexing_frequency_seconds] = 30 +AppConfig[:pui_indexer_records_per_thread] = 25 +AppConfig[:pui_indexer_thread_count] = 1 + AppConfig[:allow_other_unmapped] = false AppConfig[:db_url] = proc { AppConfig.demo_db_url } @@ -132,12 +138,14 @@ # Report Configuration # :report_page_layout uses valid values for the CSS3 @page directive's # size property: http://www.w3.org/TR/css3-page/#page-size-prop -AppConfig[:report_page_layout] = "letter landscape" +AppConfig[:report_page_layout] = "letter" AppConfig[:report_pdf_font_paths] = proc { ["#{AppConfig[:backend_url]}/reports/static/fonts/dejavu/DejaVuSans.ttf"] } AppConfig[:report_pdf_font_family] = "\"DejaVu Sans\", sans-serif" # Plug-ins to load. They will load in the order specified -AppConfig[:plugins] = ['local', 'lcnaf', 'aspace-public-formats'] +AppConfig[:plugins] = ['local', 'lcnaf'] +# The aspace-public-formats plugin is not supported in the new public application +AppConfig[:plugins] << 'aspace-public-formats' unless ENV['ASPACE_PUBLIC_NEW'] == 'true' # URL to direct the feedback link # You can remove this from the footer by making the value blank. @@ -173,6 +181,12 @@ # and this AppConfig[:job_timeout_seconds] = proc { AppConfig.has_key?(:import_timeout_seconds) ? AppConfig[:import_timeout_seconds] : 300 } +# The number of concurrent threads available to run background jobs +# Introduced for AR-1619 - long running jobs were blocking the queue +# Resist the urge to set this to a big number! +AppConfig[:job_thread_count] = 2 + + # By default, only allow jobs to be cancelled if we're running against MySQL (since we can rollback) AppConfig[:jobs_cancelable] = proc { (AppConfig[:db_url] != AppConfig.demo_db_url).to_s } @@ -185,21 +199,6 @@ # this check here. Do so at your own peril. AppConfig[:ignore_schema_info_check] = false -# Jasper Reports -# (https://community.jaspersoft.com/project/jasperreports-library) -# require compilation. This can be done at startup. Please note, if you are -# using Java 8 and you want to compile at startup, keep this setting at false, -# but be sure to use the JDK version. -AppConfig[:enable_jasper] = true -AppConfig[:compile_jasper] = true - -# There are some conditions that has caused tree nodes ( ArchivalObjects, DO -# Components, and ClassificationTerms) to lose their sequence pointers and -# position setting. This will resequence these tree nodes prior to startup. -# If is recogmended that this be used very infrequently and should not be set -# to true for all startups ( as it will take a considerable amount of time ) -AppConfig[:resequence_on_startup] = false - # This is a URL that points to some demo data that can be used for testing, # teaching, etc. To use this, set an OS environment variable of ASPACE_DEMO = true AppConfig[:demo_data_url] = "https://s3-us-west-2.amazonaws.com/archivesspacedemo/latest-demo-data.zip" @@ -214,3 +213,222 @@ # you're using that AppConfig[:jetty_response_buffer_size_bytes] = 64 * 1024 AppConfig[:jetty_request_buffer_size_bytes] = 64 * 1024 + +# Define the fields for a record type that are inherited from ancestors +# if they don't have a value in the record itself. +# This is used in common/record_inheritance.rb and was developed to support +# the new public UI application. +# Note - any changes to record_inheritance config will require a reindex of pui +# records to take affect. To do this remove files from indexer_pui_state +AppConfig[:record_inheritance] = { + :archival_object => { + :inherited_fields => [ + { + :property => 'title', + :inherit_directly => true + }, + { + :property => 'component_id', + :inherit_directly => false + }, + { + :property => 'language', + :inherit_directly => true + }, + { + :property => 'dates', + :inherit_directly => true + }, + { + :property => 'extents', + :inherit_directly => true + }, + { + :property => 'linked_agents', + :inherit_if => proc {|json| json.select {|j| j['role'] == 'creator'} }, + :inherit_directly => false + }, + { + :property => 'notes', + :inherit_if => proc {|json| json.select {|j| j['type'] == 'accessrestrict'} }, + :inherit_directly => true + }, + { + :property => 'notes', + :inherit_if => proc {|json| json.select {|j| j['type'] == 'scopecontent'} }, + :inherit_directly => false + }, + ] + } +} + +# To enable composite identifiers - added to the merged record in a property _composite_identifier +# The values for :include_level and :identifier_delimiter shown here are the defaults +# If :include_level is set to true then level values (eg Series) will be included in _composite_identifier +# The :identifier_delimiter is used when joining the four part identifier for resources +#AppConfig[:record_inheritance][:archival_object][:composite_identifiers] = { +# :include_level => false, +# :identifier_delimiter => ' ' +#} + +# To configure additional elements to be inherited use this pattern in your config +#AppConfig[:record_inheritance][:archival_object][:inherited_fields] << +# { +# :property => 'linked_agents', +# :inherit_if => proc {|json| json.select {|j| j['role'] == 'subject'} }, +# :inherit_directly => true +# } +# ... or use this pattern to add many new elements at once +#AppConfig[:record_inheritance][:archival_object][:inherited_fields].concat( +# [ +# { +# :property => 'subjects', +# :inherit_if => proc {|json| +# json.select {|j| +# ! j['_resolved']['terms'].select { |t| t['term_type'] == 'topical'}.empty? +# } +# }, +# :inherit_directly => true +# }, +# { +# :property => 'external_documents', +# :inherit_directly => false +# }, +# { +# :property => 'rights_statements', +# :inherit_directly => false +# }, +# { +# :property => 'instances', +# :inherit_directly => false +# }, +# ]) + +# If you want to modify any of the default rules, the safest approach is to uncomment +# the entire default record_inheritance config and make your changes. +# For example, to stop scopecontent notes from being inherited into file or item records +# uncomment the entire record_inheritance default config above, and add a skip_if +# clause to the scopecontent rule, like this: +# { +# :property => 'notes', +# :skip_if => proc {|json| ['file', 'item'].include?(json['level']) }, +# :inherit_if => proc {|json| json.select {|j| j['type'] == 'scopecontent'} }, +# :inherit_directly => false +# }, + +# PUI Configurations +# TODO: Clean up configuration options + +AppConfig[:pui_search_results_page_size] = 25 +AppConfig[:pui_branding_img] = '/img/Aspace-logo.png' +AppConfig[:pui_block_referrer] = true # patron privacy; blocks full 'referer' when going outside the domain + +# The following determine which 'tabs' are on the main horizontal menu +AppConfig[:pui_hide] = {} +AppConfig[:pui_hide][:repositories] = false +AppConfig[:pui_hide][:resources] = false +AppConfig[:pui_hide][:digital_objects] = false +AppConfig[:pui_hide][:accessions] = false +AppConfig[:pui_hide][:subjects] = false +AppConfig[:pui_hide][:agents] = false +AppConfig[:pui_hide][:classifications] = false +# The following determine globally whether the various "badges" appear on the Repository page +# can be overriden at repository level below (e.g.: AppConfig[:repos][{repo_code}][:hide][:counts] = true +AppConfig[:pui_hide][:resource_badge] = false +AppConfig[:pui_hide][:record_badge] = false +AppConfig[:pui_hide][:subject_badge] = false +AppConfig[:pui_hide][:agent_badge] = false +AppConfig[:pui_hide][:classification_badge] = false +AppConfig[:pui_hide][:counts] = false +# Other usage examples: +# Don't display the accession ("unprocessed material") link on the main navigation menu +# AppConfig[:pui_hide][:accessions] = true + +# the following determine when the request button gets greyed out/disabled +AppConfig[:pui_requests_permitted_for_containers_only] = false # set to 'true' if you want to disable if there is no top container + +# Repository-specific examples. We are using the imaginary repository code of 'foo'. Note the lower-case +AppConfig[:pui_repos] = {} +# Example: +# AppConfig[:pui_repos][{repo_code}] = {} +# AppConfig[:pui_repos][{repo_code}][:requests_permitted_for_containers_only] = true # for a particular repository ,disable request +# AppConfig[:pui_repos][{repo_code}][:request_email] = {email address} # the email address to send any repository requests +# AppConfig[:pui_repos][{repo_code}][:hide] = {} +# AppConfig[:pui_repos][{repo_code}][:hide][:counts] = true + +AppConfig[:pui_display_deaccessions] = true + +# Enable / disable PUI resource/archival object page actions +AppConfig[:pui_page_actions_cite] = true +AppConfig[:pui_page_actions_bookmark] = true +AppConfig[:pui_page_actions_request] = true +AppConfig[:pui_page_actions_print] = true + +# Add page actions via the configuration +AppConfig[:pui_page_custom_actions] = [] +# Examples: +# Javascript action example: +# AppConfig[:pui_page_custom_actions] << { +# 'record_type' => ['resource', 'archival_object'], # the jsonmodel type to show for +# 'label' => 'actions.do_something', # the I18n path for the action button +# 'icon' => 'fa-paw', # the font-awesome icon CSS class +# 'onclick_javascript' => 'alert("do something grand");', +# } +# # Hyperlink action example: +# AppConfig[:pui_page_custom_actions] << { +# 'record_type' => ['resource', 'archival_object'], # the jsonmodel type to show for +# 'label' => 'actions.do_something', # the I18n path for the action button +# 'icon' => 'fa-paw', # the font-awesome icon CSS class +# 'url_proc' => proc {|record| 'http://example.com/aspace?uri='+record.uri}, +# } +# # Form-POST action example: +# AppConfig[:pui_page_custom_actions] << { +# 'record_type' => ['resource', 'archival_object'], # the jsonmodel type to show for +# 'label' => 'actions.do_something', # the I18n path for the action button +# 'icon' => 'fa-paw', # the font-awesome icon CSS class +# # 'post_params_proc' returns a hash of params which populates a form with hidden inputs ('name' => 'value') +# 'post_params_proc' => proc {|record| {'uri' => record.uri, 'display_string' => record.display_string} }, +# # 'url_proc' returns the URL for the form to POST to +# 'url_proc' => proc {|record| 'http://example.com/aspace?uri='+record.uri}, +# # 'form_id' as string to be used as the form's ID +# 'form_id' => 'my_grand_action', +# } +# # ERB action example: +# AppConfig[:pui_page_custom_actions] << { +# 'record_type' => ['resource', 'archival_object'], # the jsonmodel type to show for +# # 'erb_partial' returns the path to an erb template from which the action will be rendered +# 'erb_partial' => 'shared/my_special_action', +# } + +# PUI email settings (logs emails when disabled) +AppConfig[:pui_email_enabled] = false + +# See above AppConfig[:pui_repos][{repo_code}][:request_email] for setting repository email overrides +# 'pui_email_override' for testing, this email will be the to-address for all sent emails +# AppConfig[:pui_email_override] = 'testing@example.com' +# 'pui_request_email_fallback_to_address' the 'to' email address for repositories that don't define their own email +#AppConfig[:pui_request_email_fallback_to_address] = 'testing@example.com' +# 'pui_request_email_fallback_from_address' the 'from' email address for repositories that don't define their own email +#AppConfig[:pui_request_email_fallback_from_address] = 'testing@example.com' + +# Example sendmail configuration: +# AppConfig[:pui_email_delivery_method] = :sendmail +# AppConfig[:pui_email_sendmail_settings] = { +# location: '/usr/sbin/sendmail', +# arguments: '-i' +# } +#AppConfig[:pui_email_perform_deliveries] = true +#AppConfig[:pui_email_raise_delivery_errors] = true +# Example SMTP configuration: +#AppConfig[:pui_email_delivery_method] = :smtp +#AppConfig[:pui_email_smtp_settings] = { +# address: 'smtp.gmail.com', +# port: 587, +# domain: 'gmail.com', +# user_name: '<username>', +# password: '<password>', +# authentication: 'plain', +# enable_starttls_auto: true, +#} +#AppConfig[:pui_email_perform_deliveries] = true +#AppConfig[:pui_email_raise_delivery_errors] = true diff --git a/common/config/config-distribution.rb b/common/config/config-distribution.rb index 928c7334b2..0c237e938d 100644 --- a/common/config/config-distribution.rb +++ b/common/config/config-distribution.rb @@ -63,20 +63,23 @@ def self.get_preferred_config_path if java.lang.System.getProperty("aspace.config") # Explicit Java property java.lang.System.getProperty("aspace.config") - elsif java.lang.System.getProperty("ASPACE_LAUNCHER_BASE") && - File.exists?(File.join(java.lang.System.getProperty("ASPACE_LAUNCHER_BASE"), "config", "config.rb")) - File.join(java.lang.System.getProperty("ASPACE_LAUNCHER_BASE"), "config", "config.rb") + elsif ENV['ASPACE_LAUNCHER_BASE'] && File.exist?(File.join(ENV['ASPACE_LAUNCHER_BASE'], "config", "config.rb")) + File.join(ENV['ASPACE_LAUNCHER_BASE'], "config", "config.rb") elsif java.lang.System.getProperty("catalina.base") # Tomcat users File.join(java.lang.System.getProperty("catalina.base"), "conf", "config.rb") elsif __FILE__.index(java.lang.System.getProperty("java.io.tmpdir")) != 0 - File.join(File.dirname(__FILE__), "config.rb") + File.join(get_devserver_base, "config", "config.rb") else File.join(Dir.home, ".aspace_config.rb") end end + def self.get_devserver_base + File.join(ENV.fetch("GEM_HOME"), "..", "..") + end + def self.find_user_config possible_locations = [ get_preferred_config_path, @@ -85,7 +88,7 @@ def self.find_user_config ] possible_locations.each do |config| - if config and File.exists?(config) + if config and File.exist?(config) return config end end diff --git a/common/db/db_migrator.rb b/common/db/db_migrator.rb index f9adafcefd..d1e365fea3 100644 --- a/common/db/db_migrator.rb +++ b/common/db/db_migrator.rb @@ -173,7 +173,7 @@ class DBMigrator PLUGIN_MIGRATION_DIRS = {} AppConfig[:plugins].each do |plugin| mig_dir = ASUtils.find_local_directories("migrations", plugin).shift - if mig_dir && Dir.exists?(mig_dir) + if mig_dir && Dir.exist?(mig_dir) PLUGIN_MIGRATIONS << plugin PLUGIN_MIGRATION_DIRS[plugin] = mig_dir end diff --git a/common/db/migrations/076_add_publish_to_repository.rb b/common/db/migrations/076_add_publish_to_repository.rb new file mode 100644 index 0000000000..9c9873adb0 --- /dev/null +++ b/common/db/migrations/076_add_publish_to_repository.rb @@ -0,0 +1,19 @@ +Sequel.migration do + + up do + alter_table(:repository) do + add_column(:publish, Integer) + end + + self[:repository].filter(:hidden => 1).update(:publish => 0) + self[:repository].filter(:hidden => 0).update(:publish => 1) + + self[:repository].update(:system_mtime => Time.now) + end + + + down do + end + +end + diff --git a/common/db/migrations/077_add_publish_and_suppress_to_classifications.rb b/common/db/migrations/077_add_publish_and_suppress_to_classifications.rb new file mode 100644 index 0000000000..96f565f68f --- /dev/null +++ b/common/db/migrations/077_add_publish_and_suppress_to_classifications.rb @@ -0,0 +1,21 @@ +Sequel.migration do + + up do + alter_table(:classification) do + add_column(:publish, Integer, :default => 1) + add_column(:suppressed, Integer, :default => 0) + end + + alter_table(:classification_term) do + add_column(:publish, Integer, :default => 1) + add_column(:suppressed, Integer, :default => 0) + end + + end + + + down do + end + +end + diff --git a/common/db/migrations/078_add_display_string_to_classification_term.rb b/common/db/migrations/078_add_display_string_to_classification_term.rb new file mode 100644 index 0000000000..793f14aa4c --- /dev/null +++ b/common/db/migrations/078_add_display_string_to_classification_term.rb @@ -0,0 +1,21 @@ +Sequel.migration do + + up do + alter_table(:classification_term) do + TextField :display_string, :null => true + end + + self[:classification_term].update(:display_string => :title) + + alter_table(:classification_term) do + set_column_not_null :display_string + end + + end + + + down do + end + +end + diff --git a/common/db/migrations/079_add_position_gaps.rb b/common/db/migrations/079_add_position_gaps.rb new file mode 100644 index 0000000000..35aaa8a62d --- /dev/null +++ b/common/db/migrations/079_add_position_gaps.rb @@ -0,0 +1,65 @@ +Sequel.migration do + + up do + alter_table(:archival_object) do + drop_index([:parent_name, :position], :unique => true, :name => "uniq_ao_pos") + end + + alter_table(:digital_object_component) do + drop_index([:parent_name, :position], :unique => true, :name => "uniq_do_pos") + end + + alter_table(:classification_term) do + drop_index([:parent_name, :position], :unique => true, :name => "uniq_ct_pos") + end + + self.transaction do + tables = [:archival_object, :digital_object_component, :classification_term] + + tables.each do |table| + # Find any positions that were NULL and give them a proper value. Since + # they would have sorted to the top, we'll arbitrarily assign them a + # number starting from 0. + + parents_with_nulls = self[table].filter(:position => nil).select(:parent_name).distinct.map {|row| row[:parent_name]} + + parents_with_nulls.each do |parent_name| + null_records = self[table].filter(:parent_name => parent_name).filter(:position => nil) + + # Make sure we have some space for our new positions + self[table].filter(:parent_name => parent_name).update(:position => Sequel.lit("position + #{null_records.count}")) + + null_records.select(:id).each_with_index do |record, new_position| + self[table].filter(:id => record[:id]).update(:position => new_position) + end + end + end + + # Multiply all positions by 1000 to introduce gaps. This will reduce the + # amount of shuffling we need to do when repositioning records. + tables.each do |table| + self[table].update(:position => Sequel.lit('position * 1000')) + end + end + + alter_table(:archival_object) do + add_index([:parent_name, :position], :unique => true, :name => "uniq_ao_pos") + set_column_not_null(:position) + end + + alter_table(:digital_object_component) do + add_index([:parent_name, :position], :unique => true, :name => "uniq_do_pos") + set_column_not_null(:position) + end + + alter_table(:classification_term) do + add_index([:parent_name, :position], :unique => true, :name => "uniq_ct_pos") + set_column_not_null(:position) + end + end + + + down do + end + +end diff --git a/common/db/migrations/080_change_job_type_to_non_enum.rb b/common/db/migrations/080_change_job_type_to_non_enum.rb new file mode 100644 index 0000000000..c330026111 --- /dev/null +++ b/common/db/migrations/080_change_job_type_to_non_enum.rb @@ -0,0 +1,27 @@ +Sequel.migration do + + up do + alter_table(:job) do + add_column(:job_type, String, :null => false, :default => 'unknown_job_type') + end + + self[:job].update(:job_type => self[:enumeration_value].filter(:id => :job_type_id).select(:value)) + + alter_table(:job) do + drop_foreign_key(:job_type_id) + end + + self.transaction do + enum = self[:enumeration].filter(:name => 'job_type').first + self[:enumeration_value].filter(:enumeration_id => enum[:id]).delete + self[:enumeration].filter(:name => 'job_type').delete + end + end + + + down do + # no going back + end + +end + diff --git a/common/db/migrations/081_update_job_permissions.rb b/common/db/migrations/081_update_job_permissions.rb new file mode 100644 index 0000000000..55629972de --- /dev/null +++ b/common/db/migrations/081_update_job_permissions.rb @@ -0,0 +1,47 @@ +require_relative 'utils' + +Sequel.migration do + + up do + self.transaction do + create_job_id = self[:permission].insert(:permission_code => 'create_job', + :description => 'The ability to create background jobs', + :level => 'repository', + :created_by => 'admin', + :last_modified_by => 'admin', + :create_time => Time.now, + :system_mtime => Time.now, + :user_mtime => Time.now) + + cancel_job_id = self[:permission].insert(:permission_code => 'cancel_job', + :description => 'The ability to cancel background jobs', + :level => 'repository', + :created_by => 'admin', + :last_modified_by => 'admin', + :create_time => Time.now, + :system_mtime => Time.now, + :user_mtime => Time.now) + + + self[:group].filter(:group_code => 'repository-basic-data-entry').select(:id).each do |group| + self[:group_permission].insert(:permission_id => create_job_id, :group_id => group[:id]) + end + + self[:group].filter(:group_code => 'repository-managers').select(:id).each do |group| + self[:group_permission].insert(:permission_id => cancel_job_id, :group_id => group[:id]) + end + + self[:group].filter(:group_code => 'administrators').select(:id).each do |group| + self[:group_permission].insert(:permission_id => create_job_id, :group_id => group[:id]) + self[:group_permission].insert(:permission_id => cancel_job_id, :group_id => group[:id]) + end + end + end + + + down do + # don't even think about it + end + +end + diff --git a/common/db/migrations/082_reindex_all_records.rb b/common/db/migrations/082_reindex_all_records.rb new file mode 100644 index 0000000000..9ee0659b24 --- /dev/null +++ b/common/db/migrations/082_reindex_all_records.rb @@ -0,0 +1,26 @@ +require_relative 'utils' + +Sequel.migration do + + up do + now = Time.now + self.transaction do + # touch all record types to trigger a reindex + # this is necessary because of the introduction of the PUI indexer, + # and a related refactoring of the indexer code + [:accession, :archival_object, :container_profile, :resource, :top_container, + :digital_object, :agent_corporate_entity, :agent_family, :agent_person, :agent_software, + :classification, :deaccession, :location, :location_profile, :repository, :subject, + :vocabulary].each do |table| + self[table].update(:system_mtime => now) + end + end + end + + + down do + # not going to happen, people + end + +end + diff --git a/common/jsonmodel.rb b/common/jsonmodel.rb index 16be2dbea0..48ce92ffdb 100644 --- a/common/jsonmodel.rb +++ b/common/jsonmodel.rb @@ -137,7 +137,7 @@ def self.schema_src(schema_name) schema = File.join(dir, "#{schema_name}.rb") - if File.exists?(schema) + if File.exist?(schema) return File.open(schema).read end end @@ -163,7 +163,6 @@ def self.allow_unmapped_enum_value(val, magic_value = 'other_unmapped') def self.load_schema(schema_name) if not @@models[schema_name] - old_verbose = $VERBOSE $VERBOSE = nil src = schema_src(schema_name) @@ -228,7 +227,7 @@ def self.load_schema(schema_name) end ASUtils.find_local_directories("schemas/#{schema_name}_ext.rb"). - select {|path| File.exists?(path)}. + select {|path| File.exist?(path)}. each do |schema_extension| entry[:schema]['properties'] = ASUtils.deep_merge(entry[:schema]['properties'], eval(File.open(schema_extension).read)) diff --git a/common/jsonmodel_client.rb b/common/jsonmodel_client.rb index e51ff095dc..a2d880dc31 100644 --- a/common/jsonmodel_client.rb +++ b/common/jsonmodel_client.rb @@ -170,10 +170,13 @@ def self.stream(uri, params = {}, &block) def self.get_json(uri, params = {}) + if params.respond_to?(:to_unsafe_hash) + params = params.to_unsafe_hash + end + uri = URI("#{backend_url}#{uri}") uri.query = URI.encode_www_form(params) - response = get_response(uri) if response.is_a?(Net::HTTPSuccess) || response.code == '200' diff --git a/common/jsonmodel_type.rb b/common/jsonmodel_type.rb index 2de6deec88..3d3c88bb46 100644 --- a/common/jsonmodel_type.rb +++ b/common/jsonmodel_type.rb @@ -23,9 +23,17 @@ def self.init(type, schema, mixins = []) end + # If a JSONModel is extended, make its schema and record type class variables + # available on the subclass too. + def self.inherited(child) + child.instance_variable_set(:@schema, schema) + child.instance_variable_set(:@record_type, record_type) + end + + # Return the JSON schema that defines this JSONModel class def self.schema - find_ancestor_class_instance(:@schema) + @schema end @@ -38,7 +46,7 @@ def self.schema_version # Return the type of this JSONModel class (a keyword like # :archival_object) def self.record_type - find_ancestor_class_instance(:@record_type) + @record_type end @@ -144,7 +152,8 @@ def self.id_for(uri, opts = {}, noerror = false) if uri =~ /#{pattern}\/#{ID_REGEXP}(\#.*)?$/ return id_to_int($1) - elsif uri =~ /#{pattern.gsub(/\[\^\/ \]\+\/tree/, '')}#{ID_REGEXP}\/tree$/ + elsif uri =~ /#{pattern.gsub(/\[\^\/ \]\+\/tree/, '')}#{ID_REGEXP}\/(tree|ordered_records)$/ + # FIXME: gross hardcoding... return id_to_int($1) else if noerror @@ -418,25 +427,6 @@ def self.substitute_parameters(uri, opts = {}) private - # If this class is subclassed, we won't be able to see our class instance - # variables unless we explicitly look up the inheritance chain. - def self.find_ancestor_class_instance(variable) - @ancestor_instance_variables ||= Atomic.new({}) - - if !@ancestor_instance_variables.value[variable] - self.ancestors.each do |clz| - val = clz.instance_variable_get(variable) - if val - @ancestor_instance_variables.update {|vs| vs.merge({variable => val})} - break - end - end - end - - @ancestor_instance_variables.value[variable] - end - - # Define accessors for all variable names listed in 'attributes' def self.define_accessors(attributes) attributes.each do |attribute| diff --git a/common/lib/jasperreports-5.6.0.jar b/common/lib/jasperreports-5.6.0.jar deleted file mode 100644 index 9ea433790f..0000000000 Binary files a/common/lib/jasperreports-5.6.0.jar and /dev/null differ diff --git a/common/lib/jasperreports-fonts-4.0.0.jar b/common/lib/jasperreports-fonts-4.0.0.jar deleted file mode 100644 index 8da09abc4a..0000000000 Binary files a/common/lib/jasperreports-fonts-4.0.0.jar and /dev/null differ diff --git a/common/locales/en.yml b/common/locales/en.yml index 2b6066bc97..666949de58 100644 --- a/common/locales/en.yml +++ b/common/locales/en.yml @@ -1422,6 +1422,8 @@ en: manage_container_record: delete/bulk update top container records manage_container_profile_record: create/update/delete container profile records manage_location_profile_record: create/update/delete location profile records + create_job: create and run a background job + cancel_job: cancel a background job _singular: Group _plural: Groups @@ -1607,6 +1609,10 @@ en: <p>A URL or other file location identifier referencing a file that contains a branding device to be used in the repository's online finding aids. A typical branding device is a university seal or logo.</p> _singular: Repository _plural: Repositories + publish: Publish? + publish_tooltip: | + <p>Determines whether this Repository and any record it contains will be published to public (patron) interfaces.</p> + term: &term_attributes # tooltips not displaying @@ -1851,18 +1857,6 @@ en: _plural: Defaults reports: - CreatedAccessionsReport: Created Accessions - UnprocessedAccessionsReport: Unprocessed Accessions - LocationHoldingsReport: Location Holdings - location_holdings: - report_title: Report on containers shelved at one or more locations - repository_report_type: Repository - building_report_type: Building - single_location_report_type: Single Location - location_range_report_type: Range of Locations - search_range: Report on a range of locations - start_range: First location - end_range: Last location parameters: from: From from_tooltip: @@ -1926,6 +1920,13 @@ en: _singular: Background Job _plural: Background Jobs job_type: Job Type + types: + print_to_pdf_job: Print To PDF + import_job: Import Data + find_and_replace_job: Batch Find and Replace (Beta) + report_job: Reports + container_conversion_job: Reports + unknown_job_type: Unknown job type status: Status status_completed: Completed status_queued: Queued @@ -1940,7 +1941,7 @@ en: import_job: _singular: Import Job - filenames: Import Files + filenames: Files import_type: Import Type import_type_ead_xml: EAD import_type_accession_csv: Accession CSV diff --git a/common/locales/enums/en.yml b/common/locales/enums/en.yml index 04fa281e31..c1cebc5af6 100644 --- a/common/locales/enums/en.yml +++ b/common/locales/enums/en.yml @@ -1397,12 +1397,6 @@ en: novalue: No value defined user_defined_enum_4: novalue: No value defined - job_type: - print_to_pdf_job: Print To PDF - import_job: Import Data - find_and_replace_job: Batch Find and Replace (Beta) - report_job: Reports - container_conversion_job: Reports dimension_units: inches: Inches feet: Feet diff --git a/common/locales/enums/es.yml b/common/locales/enums/es.yml index 18c81bff9b..788ed2cebe 100644 --- a/common/locales/enums/es.yml +++ b/common/locales/enums/es.yml @@ -1397,12 +1397,6 @@ novalue: Ningún valor definido user_defined_enum_4: novalue: Ningún valor definido - job_type: - print_to_pdf_job: Imprimir en PDF - import_job: Importar Datos - find_and_replace_job: Busca y reemplaza en bloque (Beta) - report_job: Reports - container_conversion_job: Reports dimension_units: inches: Pulgadas feet: Pies diff --git a/common/locales/es.yml b/common/locales/es.yml index 9f985bfaff..d0d32decbe 100644 --- a/common/locales/es.yml +++ b/common/locales/es.yml @@ -1923,6 +1923,12 @@ _singular: Tarea en el background _plural: Tareas en el background job_type: Tipo de tarea + types: + print_to_pdf_job: Imprimir en PDF + import_job: Importar Datos + find_and_replace_job: Busca y reemplaza en bloque (Beta) + report_job: Reports + container_conversion_job: Reports import_type: Tipo de importación import_type_ead_xml: EAD import_type_accession_csv: Acta de ingreso CSV diff --git a/common/mixed_content_parser.rb b/common/mixed_content_parser.rb index ecd302275a..ebbdcfea85 100644 --- a/common/mixed_content_parser.rb +++ b/common/mixed_content_parser.rb @@ -5,9 +5,13 @@ module MixedContentParser def self.parse(content, base_uri, opts = {} ) opts[:pretty_print] ||= false + return if content.nil? + content.strip! content.chomp! + return '' if content.empty? + # create an empty document just to get an outputSettings object # (seems like the API falls down when we do this directly...) d = org.jsoup.Jsoup.parse("") @@ -19,7 +23,13 @@ def self.parse(content, base_uri, opts = {} ) # transform blocks of text seperated by line breaks into <p> wrapped blocks content = content.split("\n\n").inject("") { |c,n| c << "<p>#{n}</p>" } if opts[:wrap_blocks] - cleaned_content = org.jsoup.Jsoup.clean(content, base_uri, org.jsoup.safety.Whitelist.relaxed.addTags("emph", "lb").addAttributes("emph", "render"), d.outputSettings()) + whitelist = org.jsoup.safety.Whitelist.relaxed + .addTags("emph", "lb", "title", "unitdate") + .addAttributes("emph", "render") + .addAttributes("title", "render") + .addAttributes("unitdate", "render") + + cleaned_content = org.jsoup.Jsoup.clean(content, base_uri, whitelist, d.outputSettings()) document = org.jsoup.Jsoup.parse(cleaned_content, base_uri, org.jsoup.parser.Parser.xmlParser()) document.outputSettings.escapeMode(Java::OrgJsoupNodes::Entities::EscapeMode.xhtml) diff --git a/common/rails_config_bug_workaround.rb b/common/rails_config_bug_workaround.rb deleted file mode 100644 index 6d98af1899..0000000000 --- a/common/rails_config_bug_workaround.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Rails::Application < Rails::Engine - - # Workaround for https://github.com/rails/rails/issues/5824 - - # Initialize the application passing the given group. By default, the - # group is :default but sprockets precompilation passes group equals - # to assets if initialize_on_precompile is false to avoid booting the - # whole app. - def initialize!(group=:default) #:nodoc: - raise "Application has been already initialized." if @initialized - run_initializers(group, self) - @initialized = true - - if config.allow_concurrency - # Force lazy initialization to avoid concurrent racing conditions - $stderr.puts("Forcing Rails configuration") - env_config - end - - self - end - - - -end diff --git a/common/record_inheritance.rb b/common/record_inheritance.rb new file mode 100644 index 0000000000..67b195160b --- /dev/null +++ b/common/record_inheritance.rb @@ -0,0 +1,217 @@ +class RecordInheritance + + def self.merge(json, opts = {}) + self.new.merge(json, opts) + end + + + def self.has_type?(type) + self.new.has_type?(type) + end + + + # Add our inheritance-specific definitions to relevant JSONModel schemas + def self.prepare_schemas + get_config.each do |record_type, config| + config[:inherited_fields].map {|fld| fld[:property]}.uniq.each do |property| + schema_def = { + 'type' => 'object', + 'subtype' => 'ref', + 'properties' => { + # Not a great type for a ref, but in this context we don't really + # know for sure what type the ancestor might be. Might need to + # think harder about this if it causes problems. + 'ref' => {'type' => 'string'}, + 'level' => {'type' => 'string'}, + 'direct' => {'type' => 'boolean'}, + } + } + + properties = JSONModel::JSONModel(record_type).schema['properties'] + if properties[property]['type'].include?('object') + add_inline_inheritance_field(properties[property], schema_def) + + elsif properties[property]['type'] == 'array' + extract_referenced_types(properties[property]['items']).each do |item_type| + if item_type['type'].include?('object') + add_inline_inheritance_field(item_type, schema_def) + else + $stderr.puts("Inheritence metadata for string arrays is not currently supported (record type: #{record_type}; property: #{property}). Please file a bug if you need this!") + end + end + else + # We add a new property alongside + properties["#{property}_inherited"] = schema_def + end + end + end + end + + + # Extract a list elements like {'type' => 'mytype'} from the various forms + # JSON schemas allow types to be in. For example: + # + # {"type" => "JSONModel(:resource) uri"} + # + # {"type" => [{"type" => "JSONModel(:resource) uri"}, + # {"type" => "JSONModel(:archival_object) uri"}]} + # + def self.extract_referenced_types(typedef) + if typedef.is_a?(Array) + typedef.map {|elt| extract_referenced_types(elt)}.flatten + elsif typedef.is_a?(Hash) + if typedef['type'].is_a?(String) + [typedef] + else + extract_referenced_types(typedef['type']) + end + else + $stderr.puts("Unrecognized type: #{typedef.inspect}") + [] + end + end + + def self.add_inline_inheritance_field(target, schema_def) + schema = nil + + if target.has_key?('properties') + schema = target['properties'] + elsif target['type'] =~ /JSONModel\(:(.*?)\) object/ + referenced_jsonmodel = $1.intern + schema = JSONModel::JSONModel(referenced_jsonmodel).schema['properties'] + end + + if schema + schema['_inherited'] = schema_def + else + $stderr.puts("Inheritence metadata for string arrays is not currently supported (property was: #{property}). Please file a bug if you need this!") + end + end + + def self.get_config + (AppConfig.has_key?(:record_inheritance) ? AppConfig[:record_inheritance] : {}) + end + + def initialize(config = nil) + @config = config || self.class.get_config + end + + + def merge(jsons, opts = {}) + return merge_record(jsons, opts) unless jsons.is_a? Array + + jsons.map do |json| + merge_record(json, opts) + end + end + + + def has_type?(type) + @config.has_key?(type.intern) + end + + + private + + + def merge_record(json_in, opts) + json = json_in.clone + + direct_only = opts.fetch(:direct_only) { false } + remove_ancestors = opts.fetch(:remove_ancestors) { false } + config = @config.fetch(json['jsonmodel_type'].intern) { false } + + if config + config[:inherited_fields].each do |fld| + next if direct_only && !fld[:inherit_directly] + + next if fld.has_key?(:skip_if) && fld[:skip_if].call(json) + + val = json[fld[:property]] + + if val.is_a?(Array) + if val.empty? || fld.has_key?(:inherit_if) && fld[:inherit_if].call(val).empty? + json['ancestors'].map {|ancestor| + ancestor_val = fld.has_key?(:inherit_if) ? fld[:inherit_if].call(ancestor['_resolved'][fld[:property]]) + : ancestor['_resolved'][fld[:property]] + unless ancestor_val.empty? + json[fld[:property]] = json[fld[:property]] + ancestor_val + json[fld[:property]].flatten! + json = apply_inheritance_properties(json, ancestor_val, ancestor, fld) + break + end + } + end + else + if !json[fld[:property]] || fld.has_key?(:inherit_if) && !fld[:inherit_if].call(json[fld[:property]]) + json['ancestors'].map {|ancestor| + ancestor_val = ancestor['_resolved'][fld[:property]] + if ancestor_val + json[fld[:property]] = ancestor_val + json = apply_inheritance_properties(json, ancestor_val, ancestor, fld) + break + end + } + end + end + end + + # composite identifer + if config.has_key?(:composite_identifiers) && !direct_only + ids = [] + json['ancestors'].reverse.each do |ancestor| + if ancestor['_resolved']['component_id'] + id = ancestor['_resolved']['component_id'] + if config[:composite_identifiers][:include_level] + id = [translate_level(ancestor['level']), id].join(' ') + end + ids << id + elsif ancestor['_resolved']['id_0'] + ids << (0..3).map { |i| ancestor['_resolved']["id_#{i}"] }.compact. + join(config[:composite_identifiers].fetch(:identifier_delimiter, ' ')) + end + end + + # include our own id in the composite if it wasn't inherited + if json['component_id'] && !json['component_id_inherited'] + id = json['component_id'] + if config[:composite_identifiers][:include_level] + id = [translate_level(json['level']), id].join(' ') + end + ids << id + end + + delimiter = config[:composite_identifiers].fetch(:identifier_delimiter, ' ') + delimiter += ' ' if delimiter != ' ' && config[:composite_identifiers][:include_level] + json['_composite_identifier'] = ids.join(delimiter) + end + end + + json['ancestors'] = [] if remove_ancestors + json + end + + + def apply_inheritance_properties(json, vals, ancestor, field_config) + props = { + 'ref' => ancestor['ref'], + 'level' => translate_level(ancestor['level']), + 'direct' => field_config[:inherit_directly] + } + + ASUtils.wrap(vals).each do |val| + if val.is_a?(Hash) + val['_inherited'] = props + else + json["#{field_config[:property]}_inherited"] = props + end + end + json + end + + + def translate_level(level) + I18n.t("enumerations.archival_record_level.#{level}", :default => level) + end + +end diff --git a/common/schemas/abstract_agent.rb b/common/schemas/abstract_agent.rb index 862b064d12..0297907829 100644 --- a/common/schemas/abstract_agent.rb +++ b/common/schemas/abstract_agent.rb @@ -42,7 +42,9 @@ "type" => "array", "items" => {"type" => [{"type" => "JSONModel(:note_bioghist) object"}]}, }, - + + "used_within_repositories" => {"type" => "array", "items" => {"type" => "JSONModel(:repository) uri"}, "readonly" => true}, + "dates_of_existence" => {"type" => "array", "items" => {"type" => "JSONModel(:date) object"}}, "publish" => {"type" => "boolean"}, diff --git a/common/schemas/advanced_query.rb b/common/schemas/advanced_query.rb index 2ceb645021..f874c02657 100644 --- a/common/schemas/advanced_query.rb +++ b/common/schemas/advanced_query.rb @@ -5,7 +5,7 @@ "type" => "object", "properties" => { - "query" => {"type" => ["JSONModel(:boolean_query) object", "JSONModel(:field_query) object", "JSONModel(:date_field_query) object", "JSONModel(:boolean_field_query) object"]}, + "query" => {"type" => ["JSONModel(:boolean_query) object", "JSONModel(:field_query) object", "JSONModel(:date_field_query) object", "JSONModel(:boolean_field_query) object", "JSONModel(:range_query) object"]}, }, }, diff --git a/common/schemas/archival_object.rb b/common/schemas/archival_object.rb index 296d50ef86..62f19e158f 100644 --- a/common/schemas/archival_object.rb +++ b/common/schemas/archival_object.rb @@ -39,9 +39,27 @@ "type" => "object", "readonly" => "true" } - } + }, + "ifmissing" => "error" }, + "ancestors" => { + "type" => "array", + "items" => { + "type" => "object", + "subtype" => "ref", + "properties" => { + "ref" => {"type" => [{"type" => "JSONModel(:resource) uri"}, + {"type" => "JSONModel(:archival_object) uri"}]}, + "level" => {"type" => "string", "maxLength" => 255}, + "_resolved" => { + "type" => "object", + "readonly" => "true" + } + } + } + }, + "series" => { "type" => "object", "subtype" => "ref", diff --git a/common/schemas/boolean_query.rb b/common/schemas/boolean_query.rb index d56fda4a27..4269d01349 100644 --- a/common/schemas/boolean_query.rb +++ b/common/schemas/boolean_query.rb @@ -6,7 +6,7 @@ "properties" => { "op" => {"type" => "string", "enum" => ["AND", "OR", "NOT"], "ifmissing" => "error"}, - "subqueries" => {"type" => ["JSONModel(:boolean_query) object", "JSONModel(:field_query) object", "JSONModel(:boolean_field_query) object", "JSONModel(:date_field_query) object"], "ifmissing" => "error", "minItems" => 1}, + "subqueries" => {"type" => ["JSONModel(:boolean_query) object", "JSONModel(:field_query) object", "JSONModel(:boolean_field_query) object", "JSONModel(:date_field_query) object", "JSONModel(:range_query) object"], "ifmissing" => "error", "minItems" => 1}, }, }, diff --git a/common/schemas/classification_term.rb b/common/schemas/classification_term.rb index c3140f590e..0d23a30484 100644 --- a/common/schemas/classification_term.rb +++ b/common/schemas/classification_term.rb @@ -6,6 +6,7 @@ "parent" => "abstract_classification", "uri" => "/repositories/:repo_id/classification_terms", "properties" => { + "display_string" => {"type" => "string", "readonly" => true}, "position" => {"type" => "integer", "required" => false}, @@ -31,7 +32,8 @@ "type" => "object", "readonly" => "true" }, - } + }, + "ifmissing" => "error" } }, }, diff --git a/common/schemas/date_field_query.rb b/common/schemas/date_field_query.rb index 4319efbc94..0a78a355f4 100644 --- a/common/schemas/date_field_query.rb +++ b/common/schemas/date_field_query.rb @@ -5,9 +5,9 @@ "type" => "object", "properties" => { - "comparator" => {"type" => "string", "enum" => ["greater_than", "lesser_than", "equal"]}, + "comparator" => {"type" => "string", "enum" => ["greater_than", "lesser_than", "equal", "empty"]}, "field" => {"type" => "string", "ifmissing" => "error"}, - "value" => {"type" => "date", "ifmissing" => "error"}, + "value" => {"type" => "date"}, "negated" => {"type" => "boolean", "default" => false}, }, diff --git a/common/schemas/digital_object_component.rb b/common/schemas/digital_object_component.rb index f270d45383..5f2a4712d5 100644 --- a/common/schemas/digital_object_component.rb +++ b/common/schemas/digital_object_component.rb @@ -35,7 +35,8 @@ "type" => "object", "readonly" => "true" } - } + }, + "ifmissing" => "error" }, "position" => {"type" => "integer", "required" => false}, diff --git a/common/schemas/field_query.rb b/common/schemas/field_query.rb index 868d07aa16..d49e60846b 100644 --- a/common/schemas/field_query.rb +++ b/common/schemas/field_query.rb @@ -7,9 +7,11 @@ "negated" => {"type" => "boolean", "default" => false}, "field" => {"type" => "string", "ifmissing" => "error"}, - "value" => {"type" => "string", "ifmissing" => "error"}, + "value" => {"type" => "string"}, "literal" => {"type" => "boolean", "default" => false}, + "comparator" => {"type" => "string", "enum" => ["contains", "empty"]}, + }, }, } diff --git a/common/schemas/job.rb b/common/schemas/job.rb index 96aa362c22..ba8225d42d 100644 --- a/common/schemas/job.rb +++ b/common/schemas/job.rb @@ -1,11 +1,3 @@ -JOB_TYPES = [ - {"type" => "JSONModel(:import_job) object"}, - {"type" => "JSONModel(:find_and_replace_job) object"}, - {"type" => "JSONModel(:print_to_pdf_job) object"}, - {"type" => "JSONModel(:report_job) object"}, - {"type" => "JSONModel(:container_conversion_job) object"} - ] - { :schema => { "$schema" => "http://www.archivesspace.org/archivesspace.json", @@ -18,13 +10,11 @@ "job_type" => { "type" => "string", - "ifmissing" => "error", - "minLength" => 1, - "dynamic_enum" => "job_type" + "readonly" => true }, - + "job" => { - "type" => JOB_TYPES + "type" => "object" }, "job_params" => { diff --git a/common/schemas/range_query.rb b/common/schemas/range_query.rb new file mode 100644 index 0000000000..ee41118736 --- /dev/null +++ b/common/schemas/range_query.rb @@ -0,0 +1,14 @@ +{ + :schema => { + "$schema" => "http://www.archivesspace.org/archivesspace.json", + "version" => 1, + "type" => "object", + "properties" => { + + "field" => {"type" => "string", "ifmissing" => "error"}, + "from" => {"type" => "string"}, + "to" => {"type" => "string"}, + + }, + }, +} diff --git a/common/schemas/repository.rb b/common/schemas/repository.rb index d60d5bdce9..8550018520 100644 --- a/common/schemas/repository.rb +++ b/common/schemas/repository.rb @@ -16,6 +16,8 @@ "image_url" => {"type" => "string", "maxLength" => 255, "pattern" => "\\Ahttps?:\\/\\/[\\\S]+\\z"}, "contact_persons" => {"type" => "string", "maxLength" => 65000}, + "publish" => {"type" => "boolean"}, + "display_string" => {"type" => "string", "readonly" => true}, "agent_representation" => { diff --git a/common/schemas/resource_ordered_records.rb b/common/schemas/resource_ordered_records.rb new file mode 100644 index 0000000000..a6f9265d62 --- /dev/null +++ b/common/schemas/resource_ordered_records.rb @@ -0,0 +1,28 @@ +{ + :schema => { + "$schema" => "http://www.archivesspace.org/archivesspace.json", + "version" => 1, + "type" => "object", + "uri" => "/repositories/:repo_id/resources/:id/ordered_records", + "properties" => { + "uris" => { + "type" => "array", + "items" => { + "type" => "object", + "subtype" => "ref", + "properties" => { + "ref" => { + "type" => [ { "type" => "JSONModel(:resource) uri"}, + { "type" => "JSONModel(:archival_object) uri" }], + "ifmissing" => "error" + }, + "_resolved" => { + "type" => "object", + "readonly" => "true" + } + } + } + }, + }, + }, +} diff --git a/common/schemas/subject.rb b/common/schemas/subject.rb index 1e97ba96a2..fa2fb8730a 100644 --- a/common/schemas/subject.rb +++ b/common/schemas/subject.rb @@ -15,6 +15,8 @@ "publish" => {"type" => "boolean", "default" => true, "readonly" => true}, + "used_within_repositories" => {"type" => "array", "items" => {"type" => "JSONModel(:repository) uri"}, "readonly" => true}, + "source" => {"type" => "string", "dynamic_enum" => "subject_source", "ifmissing" => "error"}, "scope_note" => {"type" => "string"}, diff --git a/common/search_definitions.rb b/common/search_definitions.rb index 12821d0596..8f01dc0c69 100644 --- a/common/search_definitions.rb +++ b/common/search_definitions.rb @@ -11,10 +11,8 @@ AdvancedSearch.define_field(:name => 'create_time', :type => :date, :visibility => [:staff], :solr_field => 'create_time') AdvancedSearch.define_field(:name => 'user_mtime', :type => :date, :visibility => [:staff], :solr_field => 'user_mtime') -AdvancedSearch.define_field(:name => 'system_mtime', :type => :date, :visibility => [:api], :solr_field => 'system_mtime') -AdvancedSearch.define_field(:name => 'last_modified_by', :type => :text, :visibility => [:api], :solr_field => 'last_modified_by') - AdvancedSearch.set_default(:text, 'keyword') AdvancedSearch.set_default(:boolean, 'published') AdvancedSearch.set_default(:date, 'create_time') + diff --git a/common/solr_snapshotter.rb b/common/solr_snapshotter.rb index 0e45c2b170..61a4d89320 100644 --- a/common/solr_snapshotter.rb +++ b/common/solr_snapshotter.rb @@ -25,7 +25,7 @@ def self.expire_snapshots victims.each do |backup_dir| - if File.exists?(File.join(backup_dir, "indexer_state")) + if File.exist?(File.join(backup_dir, "indexer_state")) log(:info, "Expiring old Solr snapshot: #{backup_dir}") FileUtils.rm_rf(backup_dir) else diff --git a/common/spec/jsonmodel_spec.rb b/common/spec/jsonmodel_spec.rb index 85e797e1d9..9d2030eb9f 100644 --- a/common/spec/jsonmodel_spec.rb +++ b/common/spec/jsonmodel_spec.rb @@ -102,21 +102,19 @@ class Klass }' + AppConfig[:plugins] = [] + + allow(JSONModel).to receive(:schema_src).and_return(schema) + allow(JSONModel).to receive(:schema_src).with("stub").and_return(schema) + allow(JSONModel).to receive(:schema_src).with("child_stub").and_return(child_schema) + + allow(Net::HTTP::Persistent).to receive(:new).and_return( StubHTTP.new ) + JSONModel::init(:client_mode => true, :url => "http://example.com", :strict_mode => true, :allow_other_unmapped => true) - AppConfig[:plugins] = [] - - # main schema - allow(Dir).to receive(:glob).and_return(['stub', 'child_stub']) - - allow(File).to receive(:open).with(/stub\.rb/).and_return( StringIO.new(schema) ) - allow(File).to receive(:open).with(/child_stub\.rb/).and_return( StringIO.new(child_schema) ) - allow(File).to receive(:exists?).with(/stub\.rb/).and_return true - - allow(Net::HTTP::Persistent).to receive(:new).and_return( StubHTTP.new ) @klass = Klass.new end diff --git a/common/test_utils.rb b/common/test_utils.rb index 8d0ca3ecb5..2a2481dee3 100644 --- a/common/test_utils.rb +++ b/common/test_utils.rb @@ -29,8 +29,8 @@ def self.wait_for_url(url) uri = URI(url) req = Net::HTTP::Get.new(uri.request_uri) Net::HTTP.start(uri.host, uri.port, nil, nil, nil, - :open_timeout => 3, - :read_timeout => 3) do |http| + :open_timeout => 60, + :read_timeout => 60) do |http| http.request(req) end @@ -45,7 +45,7 @@ def self.wait_for_url(url) def self.build_config_string(config) - java_opts = "" + java_opts = ENV.fetch('JAVA_OPTS', '') config.each do |key, value| java_opts += " -Daspace.config.#{key}=#{value}" end @@ -60,15 +60,13 @@ def self.build_config_string(config) end end - java_opts + " " + java_opts end def self.start_backend(port, config = {}, config_file = nil) - base = File.dirname(__FILE__) - - java_opts = "-Xmx256M -XX:MaxPermSize=128M" - java_opts += build_config_string(config) + base = File.dirname(__dir__) + java_opts = build_config_string(config) if config_file java_opts += " -Daspace.config=#{config_file}" end @@ -86,7 +84,9 @@ def self.start_backend(port, config = {}, config_file = nil) java_opts += " -Daspace.config.solr_url=http://localhost:#{config[:solr_port]}" end - pid = Process.spawn({:JAVA_OPTS => java_opts}, + java_opts += " -Xmx600m" + + pid = Process.spawn({'JAVA_OPTS' => java_opts}, "#{base}/../build/run", *build_args) TestUtils.wait_for_url("http://localhost:#{port}") @@ -96,9 +96,9 @@ def self.start_backend(port, config = {}, config_file = nil) def self.start_frontend(port, backend_url, config = {}) - base = File.dirname(__FILE__) + base = File.dirname(__dir__) - java_opts = "-Xmx256M -XX:MaxPermSize=128M -Daspace.config.backend_url=#{backend_url}" + java_opts = "-Daspace.config.backend_url=#{backend_url}" java_opts += build_config_string(config) build_args = ["frontend:devserver:integration", "-Daspace.frontend.port=#{port}"] @@ -107,7 +107,9 @@ def self.start_frontend(port, backend_url, config = {}) build_args << "-Dgem_home=#{ENV['GEM_HOME']}" end - pid = Process.spawn({:JAVA_OPTS => java_opts, :TEST_MODE => "true"}, + java_opts += " -Xmx1512m" + + pid = Process.spawn({'JAVA_OPTS' => java_opts, 'TEST_MODE' => "true"}, "#{base}/../build/run", *build_args) TestUtils.wait_for_url("http://localhost:#{port}") @@ -117,14 +119,14 @@ def self.start_frontend(port, backend_url, config = {}) def self.start_public(port, backend_url, config = {}) - base = File.dirname(__FILE__) + base = File.dirname(__dir__) - java_opts = "-Xmx256M -XX:MaxPermSize=128M -Daspace.config.backend_url=#{backend_url}" + java_opts = "-Daspace.config.backend_url=#{backend_url}" config.each do |key, value| java_opts += " -Daspace.config.#{key}=#{value}" end - pid = Process.spawn({:JAVA_OPTS => java_opts, :TEST_MODE => "true"}, + pid = Process.spawn({'JAVA_OPTS' => java_opts, 'TEST_MODE' => "true"}, "#{base}/../build/run", "public:devserver:integration", "-Daspace.public.port=#{port}") diff --git a/common/validations.rb b/common/validations.rb index cade93368e..73fc7f6094 100644 --- a/common/validations.rb +++ b/common/validations.rb @@ -527,4 +527,38 @@ def self.check_location_profile(hash) end end + + def self.check_field_query(hash) + errors = [] + + if (!hash.has_key?("value") || hash["value"].empty?) && hash["comparator"] != "empty" + errors << ["value", "Must specify either a value or use the 'empty' comparator"] + end + + errors + end + + if JSONModel(:field_query) + JSONModel(:field_query).add_validation("check_field_query") do |hash| + check_field_query(hash) + end + end + + + def self.check_date_field_query(hash) + errors = [] + + if (!hash.has_key?("value") || hash["value"].empty?) && hash["comparator"] != "empty" + errors << ["value", "Must specify either a value or use the 'empty' comparator"] + end + + errors + end + + if JSONModel(:date_field_query) + JSONModel(:date_field_query).add_validation("check_date_field_query") do |hash| + check_field_query(hash) + end + end + end diff --git a/docs/Gemfile b/docs/Gemfile index 2f56afc8d3..8cf8865114 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -1,3 +1,4 @@ source 'https://rubygems.org' gem "github-pages" +gem "nokogiri", "1.7.0.1" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index dee1974db2..72657301d6 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -166,7 +166,7 @@ GEM minitest (5.10.1) multipart-post (2.0.0) net-dns (0.8.0) - nokogiri (1.6.8.1) + nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) octokit (4.6.2) sawyer (~> 0.8.0, >= 0.5.3) @@ -192,10 +192,12 @@ GEM unicode-display_width (1.1.3) PLATFORMS + java ruby DEPENDENCIES github-pages + nokogiri (= 1.7.0.1) BUNDLED WITH 1.14.3 diff --git a/frontend/Gemfile b/frontend/Gemfile index 78c9970d1f..e8d3232087 100644 --- a/frontend/Gemfile +++ b/frontend/Gemfile @@ -1,44 +1,44 @@ source 'https://rubygems.org' -gem 'rails', '3.2.22.2' +gem 'rails', '5.0.1' +gem "activesupport", "5.0.1" +gem 'sprockets-rails', '2.3.3' + +gem 'tzinfo-data' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' -gem 'activerecord-jdbcsqlite3-adapter' - # Gems used only for assets and not required # in production environments by default. -group :assets do - gem 'sass-rails', '~> 3.2.3' - gem 'coffee-rails', '~> 3.2.1' - gem 'coffee-script', '= 2.3.0' - gem 'coffee-script-source', '= 1.8.0' - gem 'uglifier', '>= 1.0.3' - gem 'therubyrhino' - - gem 'less-rails', '~> 2.6.0' -end - -gem 'jquery-rails', '~> 3.1.0' -gem 'jquery-ui-rails', '~> 4.2.1' -gem "json", "1.8.0" +gem 'sass-rails' +gem 'coffee-rails' +gem 'coffee-script' +gem 'coffee-script-source' +gem 'uglifier' +gem 'therubyrhino' + +gem 'less-rails' + +gem 'jquery-rails' +gem 'jquery-ui-rails' +gem "json", "1.8.6" gem 'json-schema', '1.0.10' gem 'useragent' -gem "jruby-jars", "= 1.7.21" +gem "jruby-jars", "= 9.1.8.0" gem 'atomic', '= 1.0.1' group :test do - gem 'rspec', '~> 3.3.0' + gem 'rspec' gem 'rspec-rails' gem 'simplecov', "0.7.1" end -gem "puma", "2.8.2" +gem "mizuno", "0.6.11" gem "net-http-persistent", "2.8" gem "multipart-post", "1.2.0" @@ -46,7 +46,8 @@ gem "multipart-post", "1.2.0" gem "rubyzip", "1.0.0" gem "zip-zip", "0.3" -gem "nokogiri", '~> 1.6.1' +gem "nokogiri", "1.7.0.1" + require 'asutils' diff --git a/frontend/Gemfile.lock b/frontend/Gemfile.lock index 298507da1c..c471a1e59c 100644 --- a/frontend/Gemfile.lock +++ b/frontend/Gemfile.lock @@ -1,153 +1,183 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (3.2.22.2) - actionpack (= 3.2.22.2) - mail (~> 2.5.4) - actionpack (3.2.22.2) - activemodel (= 3.2.22.2) - activesupport (= 3.2.22.2) - builder (~> 3.0.0) + actioncable (5.0.1) + actionpack (= 5.0.1) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.0.1) + actionview (= 5.0.1) + activesupport (= 5.0.1) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.1) + activesupport (= 5.0.1) + builder (~> 3.1) erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.5) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - activemodel (3.2.22.2) - activesupport (= 3.2.22.2) - builder (~> 3.0.0) - activerecord (3.2.22.2) - activemodel (= 3.2.22.2) - activesupport (= 3.2.22.2) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activerecord-jdbc-adapter (1.3.20) - activerecord (>= 2.2) - activerecord-jdbcsqlite3-adapter (1.3.20) - activerecord-jdbc-adapter (~> 1.3.20) - jdbc-sqlite3 (>= 3.7.2, < 3.9) - activeresource (3.2.22.2) - activemodel (= 3.2.22.2) - activesupport (= 3.2.22.2) - activesupport (3.2.22.2) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) - arel (3.0.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (5.0.1) + activesupport (= 5.0.1) + globalid (>= 0.3.6) + activemodel (5.0.1) + activesupport (= 5.0.1) + activerecord (5.0.1) + activemodel (= 5.0.1) + activesupport (= 5.0.1) + arel (~> 7.0) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + arel (7.1.4) atomic (1.0.1-java) - builder (3.0.4) - coffee-rails (3.2.2) + builder (3.2.3) + childprocess (0.6.2) + ffi (~> 1.0, >= 1.0.11) + choice (0.2.0) + coffee-rails (4.2.1) coffee-script (>= 2.2.0) - railties (~> 3.2.0) - coffee-script (2.3.0) + railties (>= 4.0.0, < 5.2.x) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.8.0) + coffee-script-source (1.12.2) commonjs (0.2.7) - diff-lcs (1.2.5) + concurrent-ruby (1.0.4-java) + diff-lcs (1.3) erubis (2.7.0) execjs (2.7.0) - hike (1.2.3) + ffi (1.9.18-java) + globalid (0.3.7) + activesupport (>= 4.1.0) i18n (0.7.0) - jdbc-sqlite3 (3.8.11.2) - journey (1.0.4) - jquery-rails (3.1.4) - railties (>= 3.0, < 5.0) + jquery-rails (4.2.2) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (4.2.1) + jquery-ui-rails (6.0.1) railties (>= 3.2.16) - jruby-jars (1.7.21) - json (1.8.0-java) + jruby-jars (9.1.8.0) + json (1.8.6-java) json-schema (1.0.10) less (2.6.0) commonjs (~> 0.2.7) - less-rails (2.6.0) - actionpack (>= 3.1) + less-rails (2.8.0) + actionpack (>= 4.0) less (~> 2.6.0) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.25.1) + sprockets (> 2, < 4) + tilt + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + minitest (5.10.1) + mizuno (0.6.11) + childprocess (>= 0.2.6) + choice (>= 0.1.0) + ffi (>= 1.0.0) + rack (>= 1.0.0) multi_json (1.12.1) multipart-post (1.2.0) net-http-persistent (2.8) - nokogiri (1.6.8-java) - polyglot (0.3.5) - puma (2.8.2-java) - rack (>= 1.1, < 2.0) - rack (1.4.7) - rack-cache (1.6.1) - rack (>= 0.4) - rack-ssl (1.3.4) - rack + nio4r (1.2.1-java) + nokogiri (1.7.0.1-java) + rack (2.0.1) rack-test (0.6.3) rack (>= 1.0) - rails (3.2.22.2) - actionmailer (= 3.2.22.2) - actionpack (= 3.2.22.2) - activerecord (= 3.2.22.2) - activeresource (= 3.2.22.2) - activesupport (= 3.2.22.2) - bundler (~> 1.0) - railties (= 3.2.22.2) - railties (3.2.22.2) - actionpack (= 3.2.22.2) - activesupport (= 3.2.22.2) - rack-ssl (~> 1.3.2) + rails (5.0.1) + actioncable (= 5.0.1) + actionmailer (= 5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) + activemodel (= 5.0.1) + activerecord (= 5.0.1) + activesupport (= 5.0.1) + bundler (>= 1.3.0, < 2.0) + railties (= 5.0.1) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.2) + activesupport (>= 4.2.0, < 6.0) + nokogiri (~> 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.0.1) + actionpack (= 5.0.1) + activesupport (= 5.0.1) + method_source rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (11.2.2) - rdoc (3.12.2) - json (~> 1.4) - rspec (3.3.0) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + rspec (3.5.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-rails (3.3.3) - actionpack (>= 3.0, < 4.3) - activesupport (>= 3.0, < 4.3) - railties (>= 3.0, < 4.3) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-support (~> 3.3.0) - rspec-support (3.3.0) + rspec-support (~> 3.5.0) + rspec-rails (3.5.2) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) rubyzip (1.0.0) - sass (3.4.22) - sass-rails (3.2.6) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) + sass (3.4.23) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) simplecov (0.7.1) multi_json (~> 1.0) simplecov-html (~> 0.7.1) simplecov-html (0.7.1) - sprockets (2.2.3) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (2.3.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (>= 2.8, < 4.0) therubyrhino (2.0.4) therubyrhino_jar (>= 1.7.3) therubyrhino_jar (1.7.6) - thor (0.19.1) - tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - tzinfo (0.3.51) - uglifier (3.0.0) + thor (0.19.4) + thread_safe (0.3.5-java) + tilt (2.0.6) + tzinfo (1.2.2) + thread_safe (~> 0.1) + tzinfo-data (1.2017.1) + tzinfo (>= 1.0.0) + uglifier (3.0.4) execjs (>= 0.3.0, < 3) - useragent (0.16.7) + useragent (0.16.8) + websocket-driver (0.6.5-java) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) zip-zip (0.3) rubyzip (>= 1.0.0) @@ -155,29 +185,31 @@ PLATFORMS java DEPENDENCIES - activerecord-jdbcsqlite3-adapter + activesupport (= 5.0.1) atomic (= 1.0.1) - coffee-rails (~> 3.2.1) - coffee-script (= 2.3.0) - coffee-script-source (= 1.8.0) - jquery-rails (~> 3.1.0) - jquery-ui-rails (~> 4.2.1) - jruby-jars (= 1.7.21) - json (= 1.8.0) + coffee-rails + coffee-script + coffee-script-source + jquery-rails + jquery-ui-rails + jruby-jars (= 9.1.8.0) + json (= 1.8.6) json-schema (= 1.0.10) - less-rails (~> 2.6.0) + less-rails + mizuno (= 0.6.11) multipart-post (= 1.2.0) net-http-persistent (= 2.8) - nokogiri (~> 1.6.1) - puma (= 2.8.2) - rails (= 3.2.22.2) - rspec (~> 3.3.0) + nokogiri (= 1.7.0.1) + rails (= 5.0.1) + rspec rspec-rails rubyzip (= 1.0.0) - sass-rails (~> 3.2.3) + sass-rails simplecov (= 0.7.1) + sprockets-rails (= 2.3.3) therubyrhino - uglifier (>= 1.0.3) + tzinfo-data + uglifier useragent zip-zip (= 0.3) diff --git a/frontend/app/assets/images/archivesspace/tree_drag_handle.gif b/frontend/app/assets/images/archivesspace/tree_drag_handle.gif new file mode 100644 index 0000000000..e0e68663d2 Binary files /dev/null and b/frontend/app/assets/images/archivesspace/tree_drag_handle.gif differ diff --git a/frontend/app/assets/javascripts/ajaxtree.js.erb b/frontend/app/assets/javascripts/ajaxtree.js.erb new file mode 100644 index 0000000000..1607b95626 --- /dev/null +++ b/frontend/app/assets/javascripts/ajaxtree.js.erb @@ -0,0 +1,459 @@ +//= require jquery.ba-hashchange + +var FORM_CHANGED_KEY = 'form_changed'; +var FORM_SUBMITTED_EVENT = 'submitted'; + +function AjaxTree(tree, $pane) { + this.tree = tree; + this.$pane = $pane; + + this._ignore_hash_change = false; + + // load initial pane! + var tree_id = this.tree_id_from_hash(); + + if (tree_id == null) { + tree_id = tree.large_tree.root_tree_id; + location.hash = 'tree::' + tree_id; + } + + this.tree.large_tree.setCurrentNode(tree_id, function() { + var midpoint = (tree.large_tree.elt.height() - $('#'+tree_id).height()) / 2; + tree.large_tree.elt.scrollTo('#'+tree_id, 0, {offset: -midpoint}); + if (tree.current().is(':not(.root-row)')) { + tree.large_tree.expandNode(tree.current()); + } + }); + this.loadPaneForId(tree_id); + this.setupHashChange(); +} + +AjaxTree.prototype.setupHashChange = function() { + $(window).hashchange($.proxy(this.handleHashChange, this)); +}; + +AjaxTree.prototype.tree_id_from_hash = function() { + if (!location.hash) { + return; + } + + var tree_id = location.hash.replace(/^#(tree::)?/, ""); + + if (TreeIds.parse_tree_id(tree_id)) { + return tree_id; + } else { + return null; + } +} + +AjaxTree.prototype.handleHashChange = function(event) { + var self = this; + + if (self._ignore_hash_change) { + // ignored! and now stop ignoring.. + this._ignore_hash_change = false; + return false; + } + + var tree_id = self.tree_id_from_hash(); + + if (tree_id == null) { + return false; + } + + self.check_for_form_changes(tree_id, function() { + self.tree.large_tree.setCurrentNode(tree_id); + + if (tree.current().is(':not(.root-row)')) { + tree.large_tree.expandNode(tree.current()); + } + + self.loadPaneForId(tree_id); + }); + + return false; +}; + +AjaxTree.prototype.loadPaneForId = function(tree_id) { + var self = this; + + var params = {}; + params.inline = true; + params[self.tree.large_tree.root_node_type + '_id'] = self.tree.large_tree.root_uri; + + var parsed = TreeIds.parse_tree_id(tree_id); + var row_type = parsed.type; + var row_id = parsed.id; + + var url = APP_PATH+row_type + 's' + '/' + row_id; + + if (!self.tree.large_tree.read_only) { + url = url + "/edit"; + } + + self._ajax_the_pane(url, params, $.noop); +}; + +AjaxTree.prototype._ajax_the_pane = function(url, params, callback) { + var self = this; + + var initial_location = window.location.hash; + + self.blockout_form(); + + $.ajax({ + url: url, + type: 'GET', + data: params, + success: function(html) { + if (window.location.hash != initial_location) { + return; + } + + self.$pane.html(html); + if (!self.tree.large_tree.read_only) { + self.setup_ajax_form(); + } + $(document).trigger("loadedrecordform.aspace", [self.$pane]); + callback(); + }, + error: function(obj, errorText, errorDesc) { + $("#object_container").html("<div class='alert alert-error'><p><%= I18n.t('errors.error_tree_pane_ajax') %></p><pre>"+errorDesc+"</pre></div>"); + } + }); +} + + +AjaxTree.prototype.setup_ajax_form = function() { + var self = this; + + var $form = $("form", self.$pane); + + $form.ajaxForm({ + data: { + inline: true + }, + beforeSubmit: function(arr, $form) { + $(".btn-primary", $form).attr("disabled","disabled"); + + if ($form.data("createPlusOne")) { + arr.push({ + name: "plus_one", + value: "true", + required: false, + type: "text" + }); + } + }, + success: function(response, status, xhr) { + var shouldPlusOne = self.$pane.find('form').data('createPlusOne'); + + self.$pane.html(response); + + var $form = self.setup_ajax_form(); + + $(document).trigger("loadedrecordform.aspace", [self.$pane]); + + if ($form.find('.error').length > 0) { + self.$pane.triggerHandler(FORM_SUBMITTED_EVENT, {success: false}); + self.on_form_changed(); + $(".btn-primary, .btn-cancel", $form).removeAttr("disabled"); + } else { + self.$pane.triggerHandler(FORM_SUBMITTED_EVENT, {success: true}); + $form.data(FORM_CHANGED_KEY, false); + + var uri = $('#uri', $form).val(); + self.quietly_change_hash(TreeIds.link_url(uri)); + self.tree.large_tree.redisplayAndShow([uri], function() { + var tree_id = TreeIds.uri_to_tree_id(uri); + self.tree.large_tree.setCurrentNode(tree_id, function() { + self.tree.toolbar_renderer.notify_form_loaded($form); + if (shouldPlusOne) { + self.add_new_after(tree.current(), tree.current().data('level')); + } + }); + }); + } + + if ( $form.data("update-monitor-paused") ) { + $form.data("update-monitor-paused", false); + } + + // scroll back to the top + $.scrollTo("header"); + }, + error: function(obj, errorText, errorDesc) { + self.$pane.prepend("<div class='alert alert-error'><p><%= I18n.t('errors.error_tree_pane_ajax')%></p><pre>"+errorDesc+"</pre></div>"); + self.$pane.triggerHandler(FORM_SUBMITTED_EVENT, {success: false}); + $(".btn-primary", $form).removeAttr("disabled"); + } + }); + + $form.on('formchanged.aspace', function() { + self.on_form_changed(); + }); + + $form.on('click', '.revert-changes a', function() { + var tree_id = tree.large_tree.current_tree_id; + self.blockout_form(); + tree.toolbar_renderer.reset(); + self.loadPaneForId(tree_id); + }); + + $form.data('createPlusOne', false); + $form.on('click', '.btn-plus-one', function(event) { + event.preventDefault(); + event.stopImmediatePropagation(); + + $form.data("createPlusOne", true); + $form.triggerHandler("submit"); + }); + + self.tree.toolbar_renderer.notify_form_loaded($form); + + return $form; +}; + +AjaxTree.prototype.title_for_new_node = function() { + if (this.tree.root_record_type == 'resource') { + return "<%= I18n.t('archival_object._frontend.tree.new_record_title') %>"; + } else if (this.tree.root_record_type == 'classification') { + return "<%= I18n.t('classification_term._frontend.tree.new_record_title') %>"; + } else if (this.tree.root_record_type == 'digital_object') { + return "<%= I18n.t('digital_object_component._frontend.tree.new_record_title') %>"; + } else { + throw 'title_for_new_node does not support: ' + this.tree.root_record_type; + } +}; + +AjaxTree.prototype.add_new_after = function(node, level) { + var self = this; + + // update the hash + self.quietly_change_hash('new'); + + // clear the toolbar + $(self.tree.toolbar_renderer.container).empty(); + + // create a new table row + var $new_tr = $('<tr>'); + $new_tr.data('last_node', node); + var colspan = 0; + node.find('td').filter(':not(.title,.drag-handle,.no-drag-handle)').each(function(td) { + colspan += $(td).attr('colspan') || 1; + }); + var $drag = $('<td>').addClass('no-drag-handle'); + $drag.appendTo($new_tr); + var $titleCell = $('<td>').addClass('title'); + var $indentor = $('<span>').addClass('indentor'); + $indentor.append($('<span>').addClass('glyphicon glyphicon-asterisk')); + $indentor.appendTo($titleCell); + $titleCell.append($('<span tabindex="0">') + .addClass('record-title') + .text(self.title_for_new_node())); + $titleCell.appendTo($new_tr); + $('<td>').attr('colspan', colspan).appendTo($new_tr); + node.removeClass('current'); + $new_tr.addClass('current'); + $new_tr.attr('id', 'new'); + + $new_tr.addClass('indent-level-'+level); + + var target_position = 0; + var parent_id = null; + + var root_node = $('#'+this.tree.large_tree.root_tree_id); + var root_uri_parts = TreeIds.uri_to_parts(root_node.data('uri')); + var node_uri_parts = TreeIds.uri_to_parts(node.data('uri')); + + // add the new row at the end of the target level + if (level > node.data('level')) { + /* We're adding a new child */ + parent_id = node_uri_parts.id; + + if (node.data('child_count') == 0) { + /* Adding a child to a currently childless element */ + node.after($new_tr); + $new_tr.find('.record-title')[0].focus(); + } else { + /* Adding a child to something with existing children */ + var callback = function() { + var endmarker = node.nextAll('.waypoint.indent-level-'+level+', .end-marker.indent-level-'+level).last(); + endmarker.after($new_tr); + $new_tr.find('.record-title')[0].focus(); + }; + + if (node.data('level') == 0) { + /* Adding to a root node. No need to expand. */ + callback(); + } else { + self.tree.large_tree.expandNode(node, callback); + } + } + } else { + /* We're adding a new sibling to the end of the current level */ + var endmarker = node.nextAll('.end-marker.indent-level-'+level+':last'); + endmarker.after($new_tr); + $new_tr.find('.record-title')[0].focus(); + + parent_id = node.data('parent_id'); + } + + var params = { + inline: true + }; + + params[root_uri_parts.type + '_id'] = root_uri_parts.id; + + if (parent_id) { + params[node_uri_parts.type + '_id'] = parent_id; + } + + var url = self._new_node_form_url_for(node.data('jsonmodel_type')); + + self._ajax_the_pane(url, params, function() { + // set form_changed = true for this new form + self.$pane.find('form').data(FORM_CHANGED_KEY, true); + + self.$pane.find('.btn-cancel').on('click', function(event) { + event.preventDefault(); + var tree_id= node.attr('id'); + var uri = node.data('uri'); + self.tree.large_tree.redisplayAndShow([uri], function() { + self.tree.large_tree.setCurrentNode(tree_id); + }); + + self.quietly_change_hash('tree::'+tree_id); + self.loadPaneForId(tree_id); + }); + }); +}; + +AjaxTree.prototype.check_for_form_changes = function(target_tree_id, callback) { + var self = this; + var $form = $("form", self.$pane); + + if ($form.data(FORM_CHANGED_KEY)) { + var p = self.confirmChanges(target_tree_id); + p.done(function(proceed) { + if (proceed) { + callback(); + } else { + var current_tree_id = self.tree.large_tree.current_tree_id; + self.quietly_change_hash('tree::'+current_tree_id); + } + }); + p.fail(function(err) { + throw err; + }); + } else { + callback(); + } +}; + +AjaxTree.prototype.confirmChanges = function(target_tree_id) { + var self = this; + var $form = $("form", self.$pane); + var current_tree_id = self.tree.large_tree.current_tree_id; + + var d = $.Deferred(); + + // open save your changes modal + AS.openCustomModal("saveYourChangesModal", "<%= I18n.t('save_changes_modal.save') %>", AS.renderTemplate("save_changes_modal_template")); + + $("#saveChangesButton", "#saveYourChangesModal").on("click", function() { + $('.btn', '#saveYourChangesModal').addClass('disabled'); + + var onSubmitSuccess = function() { + $form.data(FORM_CHANGED_KEY, false); + $("#saveYourChangesModal").modal("hide"); + d.resolve(true); + }; + + var onSubmitError = function() { + $("#saveYourChangesModal").modal("hide"); + d.resolve(false); + }; + + self.$pane.one(FORM_SUBMITTED_EVENT, function(event, data) { + if (data.success) { + onSubmitSuccess(); + } else { + onSubmitError(); + } + }); + + $form.triggerHandler("submit"); + }); + + $("#dismissChangesButton", "#saveYourChangesModal").on("click", function() { + $form.data("form_changed", false); + + $("#saveYourChangesModal").modal("hide"); + var tree_id = self.tree_id_from_hash(); + var uri = $('#' + tree_id).data('uri'); + + self.tree.large_tree.redisplayAndShow([uri], function() { + self.tree.large_tree.setCurrentNode(tree_id); + }); + d.resolve(true); + }); + + $(".btn-cancel", "#saveYourChangesModal").on("click", function() { + d.resolve(false); + }); + + return d.promise(); +}; + +AjaxTree.prototype.quietly_change_hash = function(tree_id) { + this._ignore_hash_change = true; + location.hash = tree_id; +}; + + +AjaxTree.prototype.hide_form = function() { + this.$pane.hide(); +} + +AjaxTree.prototype.show_form = function() { + this.unblockout_form(); + this.$pane.show(); +} + +AjaxTree.prototype.blockout_form = function() { + var self = this; + if (self.$pane.has('.blockout').length > 0) { + return; + } + var $blockout = $('<div>').addClass('blockout'); + $blockout.height(self.$pane.height()); + // add 30 to take into account for outer margin :/ + $blockout.width(self.$pane.width() + 30); + $blockout.css('left', '-15px'); + self.$pane.prepend($blockout); +}; + +AjaxTree.prototype.unblockout_form = function() { + this.$pane.find('.blockout').remove(); +}; + +AjaxTree.prototype.on_form_changed = function() { + var $form = this.$pane.find('form'); + if (!$form.data(FORM_CHANGED_KEY)) { + $form.data(FORM_CHANGED_KEY, true); + self.tree.toolbar_renderer.notify_form_changed($form); + } +}; + +AjaxTree.prototype._new_node_form_url_for = function(jsonmodel_type) { + if (jsonmodel_type == 'resource' || jsonmodel_type == 'archival_object') { + return '<%= Rails.application.routes.url_helpers.archival_objects_path %>/new'; + } else if (jsonmodel_type == 'digital_object' || jsonmodel_type == 'digital_object_component') { + return '<%= Rails.application.routes.url_helpers.digital_object_components_path %>/new'; + } else if (jsonmodel_type == 'classification' || jsonmodel_type == 'classification_term') { + return '<%= Rails.application.routes.url_helpers.classification_terms_path %>/new'; + } else { + throw 'No new form available for: '+ jsonmodel_type; + } +}; diff --git a/frontend/app/assets/javascripts/application.js b/frontend/app/assets/javascripts/application.js index 7666a2ed6c..91b47995ab 100644 --- a/frontend/app/assets/javascripts/application.js +++ b/frontend/app/assets/javascripts/application.js @@ -11,7 +11,7 @@ // GO AFTER THE REQUIRES BELOW. // //= require jquery -//= require jquery.ui.all +//= require jquery-ui //= require jquery_ujs //= require jquery.browser //= require twitter/bootstrap diff --git a/frontend/app/assets/javascripts/header.js b/frontend/app/assets/javascripts/header.js index ec8b82318c..4c2ed44778 100644 --- a/frontend/app/assets/javascripts/header.js +++ b/frontend/app/assets/javascripts/header.js @@ -142,6 +142,21 @@ $(function() { $(this).closest(".dropdown-menu").siblings(".advanced-search-add-row-dropdown").trigger("click"); }); + + var disableAdvancedSearch = function() { + $advancedSearchForm.on("submit", function() { + return false; + }); + $(".btn-primary", $advancedSearchContainer).attr("disabled", "disabled"); + }; + + + var enableAdvancedSearch = function() { + $advancedSearchForm.off("submit"); + $(".btn-primary", $advancedSearchContainer).removeAttr("disabled"); + }; + + var addAdvancedSearchRow = function(index, type, first, query) { var field_data = { index: index, @@ -156,16 +171,35 @@ $(function() { if (type == "date") { $("#v"+index, $row).on("change", function(event) { - $(this).closest(".form-group").removeClass("has-error"); + $(this).closest(".input-group").removeClass("has-error"); - var value = $(this).val(); - var asDate = moment(value).format("YYYY-MM-DD"); - if (asDate == "Invalid date") { - $(this).closest(".form-group").addClass("has-error"); - disableAdvancedSearch(); - } else { + var dop = $("#dop"+index, $row); + if (dop.val() == 'empty') { + enableAdvancedSearch(); + return; + } + + function isValidDate(dateString) { + var dateRegex = /^\d\d\d\d\-\d\d-\d\d$/; + var isValidDateString = dateRegex.test(dateString); + + if (!isValidDateString) { + return false; + } + + var asDate = moment(dateString).format("YYYY-MM-DD"); + if (asDate == "Invalid date") { + return false; + } + + return true; + }; + + if (isValidDate($(this).val())) { enableAdvancedSearch(); - $(this).val(asDate); + } else { + $(this).closest(".input-group").addClass("has-error"); + disableAdvancedSearch(); } }); } @@ -181,16 +215,4 @@ $(function() { addAdvancedSearchRow(i, query["type"], i == 0, query); }); } - - var disableAdvancedSearch = function() { - $advancedSearchForm.on("submit", function() { - return false; - }); - $(".btn-primary", $advancedSearchContainer).attr("disabled", "disabled"); - }; - - var enableAdvancedSearch = function() { - $advancedSearchForm.off("submit"); - $(".btn-primary", $advancedSearchContainer).removeAttr("disabled"); - }; }); diff --git a/frontend/app/assets/javascripts/jobs.crud.js b/frontend/app/assets/javascripts/jobs.crud.js index 92a75dbb6d..3302952489 100644 --- a/frontend/app/assets/javascripts/jobs.crud.js +++ b/frontend/app/assets/javascripts/jobs.crud.js @@ -112,13 +112,14 @@ $(function() { }); }; - - $("#job_job_type_", $form).change(function() { + $(document).ready(function() { $("#job_form_messages", $form).empty() - if ($(this).val() === "") { + var type = $("#job_type").val(); + + if (type === "") { // - } else if ($(this).val() === "report_job") { + } else if (type === "report_job") { $("#job_form_messages", $form) .html(AS.renderTemplate("template_report_instructions")); // we disable to form... @@ -142,14 +143,14 @@ $(function() { }); initLocationReportSubForm(); - } else if ($(this).val() === "print_to_pdf_job") { + } else if (type === "print_to_pdf_job") { $("#noImportTypeSelected", $form).hide(); $("#job_type_fields", $form) .empty() .html(AS.renderTemplate("template_print_to_pdf_job", {id_path: "print_to_pdf_job", path: "print_to_pdf_job"})); $(".linker:not(.initialised)").linker(); - } else if ($(this).val() === "find_and_replace_job") { + } else if (type === "find_and_replace_job") { $("#noImportTypeSelected", $form).hide(); $("#job_form_messages", $form) .html(AS.renderTemplate("template_find_and_replace_warning")); @@ -211,7 +212,8 @@ $(function() { }); }); - } else if ($(this).val() === "import_job") { + } else if (type === "import_job") { + // } else if ($(this).val() === "import_job") { // $("#noImportTypeSelected", $form).hide(); // $("#noImportTypeSelected", $form).show(); // $("#job_filenames_", $form).hide(); @@ -242,11 +244,15 @@ $(function() { }); $("#job_import_type_", $form).trigger("change"); + } else { + $("#noImportTypeSelected", $form).hide(); + $("#job_type_fields", $form) + .empty() + .html(AS.renderTemplate("template_" + type, {id_path: type, path: type})); + $(".linker:not(.initialised)").linker(); } }); - $("#job_job_type_", $form).trigger("change"); - var handleError = function(errorHTML) { $(".job-create-form-wrapper").replaceWith(errorHTML); @@ -262,7 +268,7 @@ $(function() { $(".btn, a, :input", $form).attr("disabled", "disabled").addClass("disabled"); $progress.show(); - jobType = $('#job_job_type_', $form).val(); + var jobType = $("#job_type").val(); if (jobType === 'find_and_replace_job') { for (var i=0; i < arr.length; i++) { diff --git a/frontend/app/assets/javascripts/jstree.primary_selected.js b/frontend/app/assets/javascripts/jstree.primary_selected.js deleted file mode 100644 index 975bbacf27..0000000000 --- a/frontend/app/assets/javascripts/jstree.primary_selected.js +++ /dev/null @@ -1,98 +0,0 @@ -(function (factory) { - "use strict"; - if (typeof define === 'function' && define.amd) { - define('jstree.primary_selected', ['jquery','jstree'], factory); - } - else if(typeof exports === 'object') { - factory(require('jquery'), require('jstree')); - } - else { - factory(jQuery, jQuery.jstree); - } -}(function ($, jstree, undefined) { - "use strict"; - - if($.jstree.plugins.primary_selected) { return; } - - $.jstree.plugins.primary_selected = function (options, parent) { - - this.ensure_sole_selected = function(obj) { - obj = this.get_node(obj); - - // important! - if (this.get_selected()[0] == obj.id) - return; - - this.deselect_all(); - this.select_node(obj); - }; - - - this.primary = function(cb) { - cb(this.get_primary_selected(true)); - } - - - this.get_primary_selected_dom = function() { - return this.get_node(this.get_primary_selected(), true); - } - - this.get_primary_selected = function(full) { - var obj_id = this._data.core.primary_selected; - if (typeof(obj_id) === 'undefined') - return false; - - return full ? this.get_node(obj_id) : obj_id - }; - - this.set_primary_selected = function(obj, cb) { - if (obj === '#') { - var root = this.get_node(obj); - obj = root.children[0]; - } - - obj = this.get_node(obj); - if (!obj || obj.id === '#') { - return false; - } - - // clear the old if it exists - if (this._data.core.primary_selected) { - var old = this.get_node(this._data.core.primary_selected); - if (old) { - old.state.primary_selected = false; - var old_dom = this.get_node(old, true); - old_dom.removeClass("primary-selected"); - } - this._data.core.primary_selected = undefined; - } - - var dom = this.get_node(obj, true); - - if (!obj.state.primary_selected) { - obj.state.primary_selected = true; - this._data.core.primary_selected = obj.id - if (dom && dom.length) { - dom.addClass("primary-selected"); - } - } - - // make sure to do this last so - // those listenting to 'select_node' - // can know what's up - this.ensure_sole_selected(obj); - - if (typeof(cb) != 'undefined') { - cb(obj); - } - - }; - - - this.refresh_primary_selected = function() { - this.set_primary_selected(this.get_primary_selected()); - } - - }; - -})); diff --git a/frontend/app/assets/javascripts/jstree.select_limit.js b/frontend/app/assets/javascripts/jstree.select_limit.js deleted file mode 100644 index 9b24cd3bc1..0000000000 --- a/frontend/app/assets/javascripts/jstree.select_limit.js +++ /dev/null @@ -1,12 +0,0 @@ -(function ($) { - $.jstree.defaults.select_limit = 20; - - $.jstree.plugins.select_limit = function (options, parent) { - // own function - this.select_node = function (obj, supress_event, prevent_open) { - if(this.settings.select_limit > this.get_selected().length) { - parent.select_node.call(this, obj, supress_event, prevent_open); - } - }; - }; -})(jQuery); diff --git a/frontend/app/assets/javascripts/largetree.js.erb b/frontend/app/assets/javascripts/largetree.js.erb new file mode 100644 index 0000000000..0a6c6ba491 --- /dev/null +++ b/frontend/app/assets/javascripts/largetree.js.erb @@ -0,0 +1,887 @@ +(function (exports) { + "use strict"; + + /************************************************************************/ + /* Tree ID helpers */ + /************************************************************************/ + var TreeIds = {} + + TreeIds.uri_to_tree_id = function (uri) { + var parts = TreeIds.uri_to_parts(uri); + return parts.type + '_' + parts.id; + } + + TreeIds.uri_to_parts = function (uri) { + var last_part = uri.replace(/\/repositories\/[0-9]+\//,""); + var bits = last_part.match(/([a-z_]+)\/([0-9]+)/); + var type_plural = bits[1].replace(/\//g,'_'); + var id = bits[2]; + var type = type_plural.replace(/s$/, ''); + + return { + type: type, + id: id + }; + } + + TreeIds.backend_uri_to_frontend_uri = function (uri) { + return APP_PATH + uri.replace(/\/repositories\/[0-9]+\//, "") + } + + TreeIds.parse_tree_id = function (tree_id) { + var regex_match = tree_id.match(/([a-z_]+)([0-9]+)/); + if (regex_match == null || regex_match.length != 3) { + return; + } + + var row_type = regex_match[1].replace(/_$/, ""); + var row_id = regex_match[2]; + + return {type: row_type, id: row_id} + } + + TreeIds.link_url = function(uri) { + // convert the uri into tree-speak + return "#tree::" + TreeIds.uri_to_tree_id(uri); + }; + + exports.TreeIds = TreeIds + /************************************************************************/ + + + var SCROLL_DELAY_MS = 100; + var THRESHOLD_EMS = 300; + + function LargeTree(datasource, container, root_uri, read_only, renderer, tree_loaded_callback, node_selected_callback) { + this.source = datasource; + this.elt = container; + this.scrollTimer = undefined; + this.renderer = renderer; + + this.progressIndicator = $('<progress class="largetree-progress-indicator" />'); + this.elt.before(this.progressIndicator); + + this.elt.css('will-change', 'transform'); + + this.root_uri = root_uri; + this.root_tree_id = TreeIds.uri_to_tree_id(root_uri); + + // default to the root_id + this.current_tree_id = this.root_tree_id; + + this.read_only = read_only; + + this.waypoints = {}; + + this.node_selected_callback = node_selected_callback; + this.populateWaypointHooks = []; + + this.initEventHandlers(); + this.renderRoot(function () { + tree_loaded_callback(); + }); + } + + LargeTree.prototype.currentlyLoading = function () { + this.progressIndicator.css('visibility', 'visible'); + } + + LargeTree.prototype.doneLoading = function () { + var self = this; + setTimeout(function () { + self.progressIndicator.css('visibility', 'hidden'); + }, 0); + } + + + LargeTree.prototype.addPlugin = function (plugin) { + plugin.initialize(this); + + return plugin; + }; + + LargeTree.prototype.addPopulateWaypointHook = function (callback) { + this.populateWaypointHooks.push(callback); + }; + + LargeTree.prototype.displayNode = function (tree_id, done_callback) { + var self = this; + + var node_id = TreeIds.parse_tree_id(tree_id).id; + + var displaySelectedNode = function () { + var node = $('#' + tree_id); + + if (done_callback) { + done_callback(node); + } + }; + + if (tree_id === self.root_tree_id) { + /* We don't need to do any fetching for the root node. */ + displaySelectedNode(); + } else { + self.source.fetchPathFromRoot(node_id).done(function (paths) { + self.recursivelyPopulateWaypoints(paths[node_id], displaySelectedNode); + }); + } + }; + + LargeTree.prototype.reparentNodes = function (new_parent, nodes, position) { + var self = this; + + var scrollPosition = self.elt.scrollTop(); + var loadingMask = self.displayLoadingMask(scrollPosition) + + var parent_uri = new_parent.data('uri'); + + if (!parent_uri) { + parent_uri = this.root_uri; + } + + if (position) { + /* If any of the nodes we're moving were originally siblings that + fall before the drop target, we need to adjust the position for the + fact that everything will "shift up" when they're moved */ + var positionAdjustment = 0; + + $(nodes).each(function (idx, elt) { + var level = $(elt).data('level'); + var node_parent_uri = $(elt).prevAll('.indent-level-' + (level - 1) + ':first').data('uri'); + + if (!node_parent_uri) { + node_parent_uri = self.root_uri; + } + + if (node_parent_uri == parent_uri && $(elt).data('position') < position) { + positionAdjustment += 1; + } + }); + + position -= positionAdjustment; + } else { + position = 0; + } + + /* Record some information about the current state of the tree so we can + revert things to more-or-less how they were once we reload. */ + var uris_to_reopen = []; + + + /* Refresh the drop target */ + if (new_parent.data('uri') && !new_parent.is('.root-row')) { + uris_to_reopen.push(new_parent.data('uri')); + } + + /* Reopen the parent of any nodes we dragged from */ + $(nodes).each(function (idx, elt) { + var level = $(elt).data('level'); + var parent_uri = $(elt).prevAll('.indent-level-' + (level - 1) + ':first').data('uri'); + + if (parent_uri) { + uris_to_reopen.push(parent_uri); + } else { + /* parent was root node */ + } + }); + + /* Reopen any other nodes that were open */ + self.elt.find('.expandme .expanded').closest('tr').each(function (idx, elt) { + uris_to_reopen.push($(elt).data('uri')); + }); + + var uris_to_move = []; + $(nodes).each(function (_, elt) { + uris_to_move.push($(elt).data('uri')); + }); + + return this.source.reparentNodes(parent_uri, + uris_to_move, + position) + .done(function () { + self.redisplayAndShow(uris_to_reopen, function () { + self.considerPopulatingWaypoint(function () { + self.elt.animate({ + scrollTop: scrollPosition + }, function(){ + loadingMask.remove(); + }); + + $(nodes).each(function (i, node) { + var id = $(node).attr('id'); + self.elt.find('#' + id).addClass('reparented-highlight'); + + setTimeout(function () { + self.elt.find('#' + id).removeClass('reparented-highlight').addClass('reparented'); + }, 500); + }); + }); + }); + }); + }; + + LargeTree.prototype.displayLoadingMask = function (scrollPosition) { + var self = this; + + var loadingMask = self.elt.clone(false); + + loadingMask.on('click', function (e) { e.preventDefault(); return false; }); + + loadingMask.find('*').removeAttr('id'); + loadingMask.attr('id', 'tree-container-loading'); + loadingMask.css('z-index', 2000) + .css('position', 'absolute') + .css('left', self.elt.offset().left) + .css('top', self.elt.offset().top); + + loadingMask.width(self.elt.width()); + + var spinner = $('<div class="spinner" />'); + spinner.css('font-size', '50px') + .css('display', 'inline') + .css('z-index', 2500) + .css('position', 'fixed') + .css('margin', 0) + .css('left', '50%') + .css('top', '50%'); + + + $('body').prepend(loadingMask); + $('body').prepend(spinner); + + loadingMask.scrollTop(scrollPosition); + + return { + remove: function () { + loadingMask.remove(); + spinner.remove(); + } + }; + }; + + LargeTree.prototype.redisplayAndShow = function(uris, done_callback) { + var self = this; + + uris = $.unique(uris); + + if (!done_callback) { + done_callback = $.noop; + } + + self.renderRoot(function () { + var uris_to_reopen = uris.slice(0) + var displayedNodes = []; + + var handle_next_uri = function (node) { + if (node) { + displayedNodes.push(node); + } + + if (uris_to_reopen.length == 0) { + /* Finally, expand any nodes that haven't been expanded along the way */ + var expand_next = function (done_callback) { + if (displayedNodes.length > 0) { + var node = displayedNodes.shift(); + if (node.is('.root-row')) { + done_callback(); + } else { + self.expandNode(node, function () { + expand_next(done_callback); + }); + } + } else { + done_callback(); + } + }; + + return expand_next(function () { + return done_callback(); + }); + } + + var uri = uris_to_reopen.shift(); + var tree_id = TreeIds.uri_to_tree_id(uri); + + self.displayNode(tree_id, handle_next_uri); + }; + + handle_next_uri(); + }); + }; + + LargeTree.prototype.recursivelyPopulateWaypoints = function (path, done_callback) { + var self = this; + + /* + Here, `path` is a list of objects like: + + node: /some/uri; offset: NN + + which means "expand subtree /some/uri then populate waypoint NN". + + The top-level is special because we automatically show it as expanded, so we skip expanding the root node. + */ + + if (!path || path.length === 0) { + done_callback(); + return; + } + + var waypoint_description = path.shift(); + + var next_fn = function () { + if (!self.waypoints[waypoint_description.node]) { + /* An error occurred while expanding. */ + debugger; + return; + } + + var waypoint = self.waypoints[waypoint_description.node][waypoint_description.offset]; + + if (!waypoint) { + /* An error occurred while expanding. */ + debugger; + return; + } + + self.populateWaypoint(waypoint, function () { + self.recursivelyPopulateWaypoints(path, done_callback); + }); + }; + + if (waypoint_description.node) { + var tree_id = TreeIds.uri_to_tree_id(waypoint_description.node); + + if ($('#' + tree_id).find('.expandme').find('.expanded').length > 0) { + next_fn(); + } else { + self.toggleNode($('#' + tree_id).find('.expandme'), next_fn); + } + } else { + /* this is the root node (subtree already expanded) */ + next_fn(); + } + }; + + LargeTree.prototype.deleteWaypoints = function (parent) { + var waypoint = parent.next(); + + if (!waypoint.hasClass('waypoint')) { + /* Nothing left to burn */ + return false; + } + + if (!waypoint.hasClass('populated')) { + waypoint.remove(); + + return true; + } + + var waypointLevel = waypoint.data('level'); + + if (!waypointLevel) { + return false; + } + + /* Delete all elements up to and including the end waypoint marker */ + while (true) { + var elt = waypoint.next(); + + if (elt.length === 0) { + break; + } + + if (elt.hasClass('end-marker') && waypointLevel == elt.data('level')) { + elt.remove(); + break; + } else { + elt.remove(); + } + } + + waypoint.remove(); + + return true; + }; + + LargeTree.prototype.toggleNode = function (button, done_callback) { + var self = this; + var parent = button.closest('tr'); + + if (button.data('expanded')) { + self.collapseNode(parent, done_callback); + } else { + self.expandNode(parent, done_callback); + } + }; + + LargeTree.prototype.expandNode = function (row, done_callback) { + var self = this; + var button = row.find('.expandme'); + + if (button.data('expanded')) { + if (done_callback) { + done_callback(); + } + return; + } + + button.find('.expandme-icon').addClass('expanded'); + $(button).data('expanded', true); + + if (!row.data('uri')) { + debugger; + } + + self.source.fetchNode(row.data('uri')).done(function (node) { + self.appendWaypoints(row, row.data('uri'), node.waypoints, node.waypoint_size, row.data('level') + 1); + + if (done_callback) { + done_callback(); + } + }); + }; + + LargeTree.prototype.collapseNode = function (row, done_callback) { + var self = this; + + while (self.deleteWaypoints(row)) { + /* Remove the elements from one or more waypoints */ + } + + var button = row.find('.expandme'); + + $(button).data('expanded', false); + button.find('.expandme-icon').removeClass('expanded'); + + /* Removing elements might have scrolled something else into view */ + setTimeout(function () { + self.considerPopulatingWaypoint(); + }, 0); + + if (done_callback) { + done_callback(); + } + }; + + LargeTree.prototype.initEventHandlers = function () { + var self = this; + var currentlyExpanding = false; + + /* Content loading */ + this.elt.on('scroll', function (event) { + if (self.scrollTimer) { + clearTimeout(self.scrollTimer); + } + + var handleScroll = function () { + if (!currentlyExpanding) { + currentlyExpanding = true; + + self.considerPopulatingWaypoint(function () { + currentlyExpanding = false; + }); + } + }; + + self.scrollTimer = setTimeout(handleScroll, SCROLL_DELAY_MS); + }); + + /* Expand/collapse nodes */ + $(this.elt).on('click', '.expandme', function (e) { + e.preventDefault(); + self.toggleNode($(this)); + }); + }; + + LargeTree.prototype.makeWaypoint = function (uri, offset, indentLevel) { + var result = $('<tr class="waypoint" />'); + result.addClass('indent-level-' + indentLevel); + + result.data('level', indentLevel); + result.data('uri', uri); + result.data('offset', offset); + + if (!this.waypoints[uri]) { + this.waypoints[uri] = {}; + } + + /* Keep a lookup table of waypoints so we can find and populate them programmatically */ + this.waypoints[uri][offset] = result; + + return result; + }; + + LargeTree.prototype.appendWaypoints = function (elt, parentURI, waypointCount, waypointSize, indentLevel) { + var child_count = elt.data('child_count'); + for (var i = waypointCount - 1; i >= 0; i--) { + var waypoint = this.makeWaypoint(parentURI, i, indentLevel); + + waypoint.data('child_count_at_this_level', child_count); + + /* We force the line height to a constant 2em so we can predictably + guess how tall to make waypoints. See largetree.less for where we + set this on table.td elements. */ + waypoint.css('height', (waypointSize * 2) + 'em'); + elt.after(waypoint); + } + + var self = this; + setTimeout(function () {self.considerPopulatingWaypoint(); }, 0); + }; + + LargeTree.prototype.renderRoot = function (done_callback) { + var self = this; + self.waypoints = {}; + + var rootList = $('<table class="root" />'); + + this.source.fetchRootNode().done(function (rootNode) { + var row = self.renderer.get_root_template(); + + row.data('uri', rootNode.uri); + row.attr('id', TreeIds.uri_to_tree_id(rootNode.uri)); + row.addClass('root-row'); + row.data('level', 0); + row.data('child_count', rootNode.child_count); + row.data('jsonmodel_type', rootNode.jsonmodel_type); + row.find('.title').append($('<a>').attr('href', TreeIds.link_url(rootNode.uri)) + .addClass('record-title') + .text(rootNode.title)); + + rootList.append(row); + self.appendWaypoints(row, null, rootNode.waypoints, rootNode.waypoint_size, 1); + + /* Remove any existing table */ + self.elt.find('table.root').remove(); + + self.elt.prepend(rootList); + self.renderer.add_root_columns(row, rootNode); + if (done_callback) { + done_callback(); + } + }); + }; + + LargeTree.prototype.considerPopulatingWaypoint = function (done_callback) { + var self = this; + + if (!done_callback) { + done_callback = $.noop; + } + + var emHeight = parseFloat($("body").css("font-size")); + var threshold_px = emHeight * THRESHOLD_EMS; + var containerTop = this.elt.offset().top; + var containerHeight = this.elt.outerHeight(); + + /* Pick a reasonable guess at which waypoint we might want to populate + (based on our scroll position) */ + var allWaypoints = self.elt.find('.waypoint'); + + if (allWaypoints.length == 0) { + done_callback(); + return; + } + + var scrollPercent = self.elt.scrollTop() / self.elt.find('table.root').height(); + var startIdx = Math.floor(scrollPercent * allWaypoints.length); + + var waypointToPopulate; + var evaluateWaypointFn = function (elt) { + /* The element's top is measured from the top of the page, but we + want it relative to the top of the container. Adjust as + appropriate. */ + var eltTop = elt.offset().top - containerTop; + var eltBottom = eltTop + elt.height(); + + var waypointVisible = (Math.abs(eltTop) <= (containerHeight + threshold_px)) || + (Math.abs(eltBottom) <= (containerHeight + threshold_px)) || + (eltTop < 0 && eltBottom > 0); + + if (waypointVisible) { + var candidate = { + elt: elt, + top: eltTop, + level: elt.data('level'), + }; + + if (!waypointToPopulate) { + waypointToPopulate = candidate; + } else { + if (waypointToPopulate.level > candidate.level || waypointToPopulate.top > candidate.top) { + waypointToPopulate = candidate; + } + } + + return true; + } else { + return false; + } + }; + + /* Search for a waypoint by scanning backwards */ + for (var i = startIdx; i >= 0; i--) { + var waypoint = $(allWaypoints[i]); + + if (waypoint.hasClass('populated')) { + /* nothing to do for this one */ + continue; + } + + var waypointWasVisible = evaluateWaypointFn(waypoint); + + if (!waypointWasVisible && i < startIdx) { + /* No point considering waypoints even further up in the DOM */ + break; + } + } + + /* Now scan forwards */ + for (var i = startIdx + 1; i < allWaypoints.length; i++) { + var waypoint = $(allWaypoints[i]); + + if (waypoint.hasClass('populated')) { + /* nothing to do for this one */ + continue; + } + + var waypointWasVisible = evaluateWaypointFn(waypoint); + + if (!waypointWasVisible) { + /* No point considering waypoints even further up in the DOM */ + break; + } + } + + if (waypointToPopulate) { + self.currentlyLoading(); + self.populateWaypoint(waypointToPopulate.elt, function () { + setTimeout(function () { + self.considerPopulatingWaypoint(done_callback); + }, 0); + }); + } else { + self.doneLoading(); + done_callback(); + } + }; + + var activeWaypointPopulates = {}; + + LargeTree.prototype.populateWaypoint = function (elt, done_callback) { + if (elt.hasClass('populated')) { + done_callback(); + return; + } + + var self = this; + var uri = elt.data('uri'); + var offset = elt.data('offset'); + var level = elt.data('level'); + + var key = uri + "_" + offset; + if (activeWaypointPopulates[key]) { + return; + } + + activeWaypointPopulates[key] = true; + + this.source.fetchWaypoint(uri, offset).done(function (nodes) { + var endMarker = self.renderer.endpoint_marker(); + endMarker.data('level', level); + endMarker.data('child_count_at_this_level', elt.data('child_count_at_this_level')); + endMarker.addClass('indent-level-' + level); + + elt.after(endMarker); + + var newRows = []; + var current = undefined; + + $(nodes).each(function (idx, node) { + var row = self.renderer.get_node_template(); + + row.addClass('largetree-node indent-level-' + level); + row.data('level', level); + row.data('child_count', node.child_count); + + var title = row.find('.title'); + title.append($('<a class="record-title" />').prop('href', TreeIds.link_url(node.uri)).text(node.title)); + title.attr('title', node.title); + + if (node.child_count === 0) { + row.find('.expandme').css('visibility', 'hidden'); + } + + self.renderer.add_node_columns(row, node); + + var tree_id = TreeIds.uri_to_tree_id(node.uri); + + row.data('uri', node.uri); + row.data('jsonmodel_type', node.jsonmodel_type); + row.data('position', node.position); + row.data('parent_id', node.parent_id); + row.attr('id', tree_id); + + if (self.current_tree_id == tree_id) { + row.addClass('current'); + current = row; + } else { + row.removeClass('current'); + } + + newRows.push(row); + }); + + elt.after.apply(elt, newRows); + + elt.addClass('populated'); + + activeWaypointPopulates[key] = false; + + $(self.populateWaypointHooks).each(function (idx, hook) { + hook(); + }); + + if (current) { + $.proxy(self.node_selected_callback, self)(current, self); + } + + done_callback(); + }); + }; + + /*********************************************************************************/ + /* Data source */ + /*********************************************************************************/ + function TreeDataSource(baseURL) { + this.url = baseURL.replace(/\/+$/, ""); + } + + + TreeDataSource.prototype.urlFor = function (action) { + return this.url + "/" + action; + }; + + TreeDataSource.prototype.fetchRootNode = function () { + var self = this; + + return $.ajax(this.urlFor("root"), + { + method: "GET", + }) + .done(function (rootNode) { + self.cachePrecomputedWaypoints(rootNode); + }); + }; + + TreeDataSource.prototype.fetchNode = function (uri) { + var self = this; + + if (!uri) { + throw "Node can't be empty!"; + } + + return $.ajax(this.urlFor("node"), + { + method: "GET", + data: { + /* THINKME: Should rename node to node_uri? S */ + node: uri, + } + }) + .done(function (node) { + self.cachePrecomputedWaypoints(node); + }); + + }; + + TreeDataSource.prototype.fetchPathFromRoot = function (node_id) { + var self = this; + + return $.ajax(this.urlFor("node_from_root"), + { + method: "GET", + data: { + node_ids: [node_id], + } + }); + }; + + TreeDataSource.prototype.fetchWaypoint = function (uri, offset) { + var cached = this.getPrecomputedWaypoint(uri, offset); + + if (cached) { + return { + done: function (callback) { + callback(cached); + } + }; + } else { + return $.ajax(this.urlFor("waypoint"), + { + method: "GET", + data: { + node: uri, + offset: offset, + } + }); + } + }; + + TreeDataSource.prototype.reparentNodes = function (new_parent_uri, node_uris, position) { + var target = TreeIds.backend_uri_to_frontend_uri(new_parent_uri); + + return $.ajax(target + "/accept_children", + { + method: 'POST', + data: { + children: node_uris, + index: position, + } + }); + }; + + var precomputedWaypoints = {}; + + TreeDataSource.prototype.getPrecomputedWaypoint = function (uri, offset) { + var result; + + if (uri === null) { + uri = ""; + } + + if (precomputedWaypoints[uri] && precomputedWaypoints[uri][offset]) { + result = precomputedWaypoints[uri][offset]; + precomputedWaypoints[uri] = {}; + } + + return result; + }; + + TreeDataSource.prototype.cachePrecomputedWaypoints = function (node) { + $(Object.keys(node.precomputed_waypoints)).each(function (idx, uri) { + precomputedWaypoints[uri] = node.precomputed_waypoints[uri]; + }); + }; + + LargeTree.prototype.setCurrentNode = function(tree_id, callback) { + $('#'+this.current_tree_id, this.elt).removeClass('current'); + this.current_tree_id = tree_id; + + if ($('#'+this.current_tree_id, this.elt).length == 1) { + var current = $('#'+this.current_tree_id, this.elt); + current.addClass('current'); + $.proxy(this.node_selected_callback, self)(current, this); + if (callback) { + callback(); + } + } else { + this.displayNode(this.current_tree_id, callback); + } + }; + + exports.LargeTree = LargeTree; + exports.TreeDataSource = TreeDataSource; + +}(window)); diff --git a/frontend/app/assets/javascripts/largetree_dragdrop.js.erb b/frontend/app/assets/javascripts/largetree_dragdrop.js.erb new file mode 100644 index 0000000000..7fac5795ab --- /dev/null +++ b/frontend/app/assets/javascripts/largetree_dragdrop.js.erb @@ -0,0 +1,475 @@ +(function (exports) { + var DRAG_DELAY = 100; + var MOUSE_OFFSET = 20; + var EXPAND_DELAY = 200; + var HOTSPOT_HEIGHT = 200; + var AUTO_SCROLL_SPEED = 200; + + function LargeTreeDragDrop() { + this.dragActive = false; + this.dragIndicator = $('<div class="tree-drag-indicator" />'); + this.rowsToMove = []; + + this.scrollUpHotspot = $('<div class="tree-scroll-hotspot tree-scroll-up-hotspot" />'); + this.scrollDownHotspot = $('<div class="tree-scroll-hotspot tree-scroll-down-hotspot" />'); + + this.dragDelayTimer = undefined; + this.expandTimer = undefined; + this.autoScrollTimer = undefined; + + this.lastCursorPosition = undefined; + } + + LargeTreeDragDrop.prototype.isDropAllowed = function(target_node) { + var self = this; + + if (target_node.is('.root-row')) { + // always able to drop onto root node + return true; + } + + var uris_to_check = []; + uris_to_check.push(target_node.data('uri')); + var level = target_node.data('level') - 1; + var row = target_node; + while (level > 0) { + row = row.prevAll('.largetree-node.indent-level-' + level + ':first'); + uris_to_check.push(row.data('uri')); + level -= 1; + } + + var isAllowed = true; + + $(self.rowsToMove).each(function (idx, selectedRow) { + var uri = $(selectedRow).data('uri'); + if ($.inArray(uri, uris_to_check) >= 0) { + isAllowed = false; + return; + } + }); + + return isAllowed; + }; + + LargeTreeDragDrop.prototype.setDragHandleState = function () { + var self = this; + + $('.drag-handle.drag-disabled', self.largetree.elt).removeClass('drag-disabled'); + $('.multiselected-row', self.largetree.elt).removeClass('multiselected-row'); + + /* Mark the children of each selected row as unselectable */ + $(self.rowsToMove).each(function (idx, selectedRow) { + var waypoint = $(selectedRow).next(); + + while (waypoint.hasClass('waypoint')) { + if (waypoint.hasClass('populated')) { + var startLevel = waypoint.data('level'); + + var elt = waypoint.next(); + while (!elt.hasClass('end-marker') && elt.data('level') >= startLevel) { + elt.find('.drag-handle').addClass('drag-disabled'); + elt = elt.next(); + } + + waypoint = elt.next(); + } else { + waypoint = waypoint.next(); + } + } + }); + + /* Mark the ancestors of each selected row as unselectable */ + $(self.rowsToMove).each(function (idx, selectedRow) { + var next = $(selectedRow); + var level = next.data('level') - 1; + + while (level > 0) { + next = next.prevAll('.largetree-node.indent-level-' + level + ':first'); + next.find('.drag-handle').addClass('drag-disabled'); + level -= 1; + } + }); + + /* Highlight selected rows */ + $('.multiselected', self.largetree.elt).closest('tr').addClass('multiselected-row'); + }; + + LargeTreeDragDrop.prototype.handleMultiSelect = function (selection) { + var self = this; + var row = selection.closest('tr'); + + if (selection.hasClass('multiselected')) { + /* deselect a selected item */ + self.rowsToMove = self.rowsToMove.filter(function (elt) { + return (elt != row[0]); + }); + + selection.removeClass('multiselected'); + } else { + /* Add this item to the selection */ + selection.addClass('multiselected'); + self.rowsToMove.push(row[0]); + } + + self.setDragHandleState(); + + return false; + }; + + LargeTreeDragDrop.prototype.handleShiftSelect = function (selection) { + var self = this; + + var row = selection.closest('tr'); + var lastSelection = self.rowsToMove[self.rowsToMove.length - 1] + + if (lastSelection) { + var start = $(lastSelection); + var end = row; + + if (start.index() > end.index()) { + /* Oops. Swap them. */ + var tmp = end; + end = start; + start = tmp; + } + + var rowsInRange = start.nextUntil(end).andSelf().add(end); + var targetLevel = $(lastSelection).data('level'); + + rowsInRange.each(function (i, row) { + if ($(row).is('.largetree-node')) { + if (!$(row).is('.multiselected') && $(row).data('level') === targetLevel) { + $(row).find('.drag-handle').addClass('multiselected') + self.rowsToMove.push(row); + } + } + }); + + self.rowsToMove = $.unique(self.rowsToMove); + + self.setDragHandleState(); + } + + return false; + }; + + LargeTreeDragDrop.prototype.initialize = function (largetree) { + var self = this; + + self.largetree = largetree; + + largetree.addPopulateWaypointHook(function () { + /* Make sure none of the descendants of any multi-selected node can + be selected */ + self.setDragHandleState(); + }); + + $(largetree.elt).on('mousedown', '.drag-handle', function (event) { + var selection = $(this); + + if (event.ctrlKey || event.metaKey) { + return self.handleMultiSelect(selection); + } else if (event.shiftKey) { + return self.handleShiftSelect(selection); + } + + self.dragDelayTimer = setTimeout(function () { + self.dragDelayTimer = undefined; + + /* Start a drag of one or more items */ + var row = selection.closest('tr'); + + if ($('.multiselected', largetree.elt).length > 0) { + if (!row.find('.drag-handle').hasClass('multiselected')) { + /* If the item we started dragging from wasn't part of + the multiselection, add it in. */ + row.find('.drag-handle').addClass('multiselected'); + self.rowsToMove.push(row[0]); + } + } else { + /* We're just tragging a single row */ + self.rowsToMove = [row[0]]; + row.addClass('multiselected'); + } + + self.setDragHandleState(); + + self.dragActive = true; + + self.scrollUpHotspot.width(largetree.elt.width()).height(HOTSPOT_HEIGHT); + self.scrollDownHotspot.width(largetree.elt.width()).height(HOTSPOT_HEIGHT); + + self.scrollUpHotspot.css('top', largetree.elt.offset().top - HOTSPOT_HEIGHT) + .css('left', largetree.elt.offset().left); + self.scrollDownHotspot.css('top', largetree.elt.offset().top + largetree.elt.height()) + .css('left', largetree.elt.offset().left); + + self.dragIndicator.empty().hide(); + self.dragIndicator.append($('<ul />').append(self.rowsToMove.map(function (elt, idx) { + return $('<li />').text($(elt).find('.title').text()); + }))); + + + $(largetree.elt).focus(); + + $('body').prepend(self.dragIndicator); + $('body').prepend(self.scrollUpHotspot); + $('body').prepend(self.scrollDownHotspot); + }, DRAG_DELAY); + + return false; + }); + + $(document).on('mousedown', function (event) { + if (!event.ctrlKey && + /* Not clicking on a drag handle */ + !$(event.target).hasClass('drag-handle') && + + /* Not operating the dropdown menu */ + $(event.target).closest('.largetree-dropdown-menu').length === 0 && + + /* Not attempting to expand something */ + $(event.target).closest('.expandme').length === 0 && + + /* Not using the resize handle */ + $(event.target).closest('.ui-resizable-handle').length === 0) { + + $(largetree.elt).find('.multiselected').removeClass('multiselected'); + self.rowsToMove = []; + + self.setDragHandleState(); + } + }); + + $(document).on('mousemove', function (event) { + if (self.dragActive) { + self.lastCursorPosition = {x: event.clientX, y: event.clientY}; + + self.dragIndicator[0].style.left = (event.clientX + MOUSE_OFFSET) + 'px'; + self.dragIndicator[0].style.top = (event.clientY + MOUSE_OFFSET) + 'px'; + self.dragIndicator[0].style.display = 'inline-block'; + } + }); + + $(largetree.elt).on('mouseout', '.expandme', function (event) { + if (self.expandTimer) { + clearTimeout(self.expandTimer); + self.expandTimer = undefined; + } + }); + + $(largetree.elt).on('mouseover', '.expandme', function (event) { + var button = $(this); + + if (self.dragActive && button.find('.expanded').length === 0) { + self.expandTimer = setTimeout(function () { + largetree.toggleNode(button); + }, EXPAND_DELAY); + } + }); + + $(largetree.elt).on('mouseenter', 'tr.root-row, tr.largetree-node', function (event) { + if (self.dragActive) { + if (self.isDropAllowed($(this))) { + $(this).addClass('drag-drop-over'); + } else { + $(this).addClass('drag-drop-over-disallowed'); + } + } + }); + + $(largetree.elt).on('mouseleave', 'tr.root-row, tr.largetree-node', function (event) { + if (self.dragActive) { + $(this).removeClass('drag-drop-over'). + removeClass('drag-drop-over-disallowed'); + } + }); + + $(document).on('mouseenter', '.tree-scroll-hotspot', function (event) { + var hotspot = event.target; + + var direction = 1; + + if ($(hotspot).hasClass('tree-scroll-up-hotspot')) { + direction = -1; + } + + var hotspotBounds = hotspot.getBoundingClientRect(); + self.autoScrollTimer = setInterval(function () { + if (self.lastCursorPosition) { + var scrollAcceleration = (self.lastCursorPosition.y - hotspotBounds.top) / hotspotBounds.height; + + if (direction == -1) { + scrollAcceleration = (1 - scrollAcceleration); + } + + /* Go faster/slower at the two extremes */ + if (scrollAcceleration > 0.8) { + scrollAcceleration += 0.1; + } + + if (scrollAcceleration < 0.2) { + scrollAcceleration = 0.05; + } + + var position = $(largetree.elt).scrollTop(); + + $(largetree.elt).scrollTop(position + (direction * AUTO_SCROLL_SPEED * scrollAcceleration)); + } + }, 50); + }); + + $(document).on('mouseout', '.tree-scroll-hotspot', function (event) { + if (self.autoScrollTimer) { + clearTimeout(self.autoScrollTimer); + } + self.autoScrollTimer = undefined; + }); + + + $(document).on('mouseup', function (event) { + if (self.dragActive) { + self.dragActive = false; + self.dragIndicator.remove(); + $(largetree.elt).find('.drag-drop-over').removeClass('drag-drop-over'); + $(largetree.elt).find('.drag-drop-over-disallowed').removeClass('drag-drop-over-disallowed'); + $(largetree.elt).find('.multiselected').removeClass('multiselected'); + + if (self.autoScrollTimer) { + clearTimeout(self.autoScrollTimer); + self.autoScrollTimer = undefined; + } + + $(document).find('.tree-scroll-hotspot').remove(); + + var dropTarget = $(event.target).closest('tr.largetree-node,tr.root-row'); + + /* If they didn't drop on a row, that's a cancel. */ + if (dropTarget.length > 0 && self.isDropAllowed(dropTarget)) { + self.handleDrop(dropTarget); + } else { + self.rowsToMove = []; + } + + self.setDragHandleState(); + + event.preventDefault(); + return false; + } + + if (self.dragDelayTimer) { + /* The mouse click finished prior to our drag starting (so we've + received a click, not a drag) */ + + clearTimeout(self.dragDelayTimer); + self.dragDelayTimer = undefined; + + /* Deselect everything */ + self.resetState(); + $(largetree.elt).find('.multiselected').removeClass('multiselected'); + + self.handleMultiSelect($(event.target)); + } + + return true; + }); + }; + + + LargeTreeDragDrop.prototype.resetState = function () { + var self = this; + + self.rowsToMove = []; + + if (self.blockout) { + self.blockout.remove(); + self.blockout = undefined; + } + + if (self.menu) { + self.menu.remove(); + self.menu = undefined; + } + self.setDragHandleState(); + }; + + LargeTreeDragDrop.prototype.handleDrop = function (dropTarget) { + var self = this; + + // blockout the page + self.blockout = $('<div>').addClass('largetree-blockout'); + $(document.body).append(self.blockout); + + // insert a menu! + self.menu = $('<ul>').addClass('dropdown-menu largetree-dropdown-menu'); + if (!dropTarget.is('.root-row')) { + self.menu.append($('<li><a href="javascript:void(0)" class="add-items-before"><%= I18n.t('tree.drag_and_drop_actions.before') %></a></li>')); + } + + self.menu.append($('<li><a href="javascript:void(0)" class="add-items-as-children"><%= I18n.t('tree.drag_and_drop_actions.as_child') %></a></li>')); + + if (!dropTarget.is('.root-row')) { + self.menu.append($('<li><a href="javascript:void(0)" class="add-items-after"><%= I18n.t('tree.drag_and_drop_actions.after') %></a></li>')); + } + + $(document.body).append(self.menu); + self.menu.css('position','absolute'); + self.menu.css('top',dropTarget.offset().top + dropTarget.height()); + self.menu.css('left',dropTarget.offset().left); + self.menu.css('z-index', 1000); + self.menu.show(); + self.menu.find('a:first').focus(); + self.menu.on('keydown', function(event) { + if (event.keyCode == 27) { //escape + self.resetState(); + return false; + } else if (event.keyCode == 38) { //up arrow + if ($(event.target).closest('li').prev().length > 0) { + $(event.target).closest('li').prev().find('a').focus(); + } + return false; + } else if (event.keyCode == 40) { //down arrow + if ($(event.target).closest('li').next().length > 0) { + $(event.target).closest('li').next().find('a').focus(); + } + return false; + } + + return true; + }); + + self.blockout.on('click', function() { + self.resetState(); + }); + + function getParent(node) { + return node.prevAll('.indent-level-'+(node.data('level') - 1) + ':first'); + } + + self.menu.on('click', '.add-items-before', function() { + self.largetree.reparentNodes(getParent(dropTarget), self.rowsToMove, dropTarget.data('position')).done(function() { + self.resetState() + }); + }).on('click', '.add-items-as-children', function() { + self.largetree.reparentNodes(dropTarget, self.rowsToMove, dropTarget.data('child_count')).done(function() { + self.resetState() + }); + }).on('click', '.add-items-after', function() { + self.largetree.reparentNodes(getParent(dropTarget), self.rowsToMove, dropTarget.data('position') + 1).done(function() { + self.resetState() + }); + }); + }; + + LargeTreeDragDrop.prototype.simulate_drag_and_drop = function(source_tree_id, target_tree_id) { + var source = $('#' + source_tree_id); + var target = $('#' + target_tree_id); + + this.rowsToMove = [source]; + this.handleDrop(target); + }; + + exports.LargeTreeDragDrop = LargeTreeDragDrop; + exports.DRAGDROP_HOTSPOT_HEIGHT = HOTSPOT_HEIGHT + +}(window)); diff --git a/frontend/app/assets/javascripts/rde.js b/frontend/app/assets/javascripts/rde.js index 2a84876b44..c802d81de6 100644 --- a/frontend/app/assets/javascripts/rde.js +++ b/frontend/app/assets/javascripts/rde.js @@ -5,7 +5,7 @@ $(function() { - $.fn.init_rapid_data_entry_form = function($modal, $node) { + $.fn.init_rapid_data_entry_form = function($modal, uri) { $(this).each(function() { var $rde_form = $(this); var $table = $("table#rdeTable", $rde_form); @@ -70,7 +70,7 @@ $(function() { COLUMN_ORDER = null; // reload the form - $(document).triggerHandler("rdeload.aspace", [$node, $modal]); + $(document).triggerHandler("rdeload.aspace", [uri, $modal]); }); $modal.on("click", ".add-row", function(event) { @@ -1269,13 +1269,15 @@ $(function() { $("select.selectpicker", $modal).selectpicker(); }; - $(document).bind("rdeload.aspace", function(event, $node, $modal) { + $(document).bind("rdeload.aspace", function(event, uri, $modal) { + var path = uri.replace(/^\/repositories\/[0-9]+\//, ''); + $.ajax({ - url: APP_PATH+$node.attr("rel")+"s/"+$node.data("id")+"/rde", + url: APP_PATH+path+"/rde", success: function(data) { $(".rde-wrapper", $modal).replaceWith("<div class='modal-body'></div>"); $(".modal-body", $modal).replaceWith(data); - $("form", "#rapidDataEntryModal").init_rapid_data_entry_form($modal, $node); + $("form", "#rapidDataEntryModal").init_rapid_data_entry_form($modal, uri); } }); }); @@ -1283,7 +1285,7 @@ $(function() { $(document).bind("rdeshow.aspace", function(event, $node, $button) { var $modal = AS.openCustomModal("rapidDataEntryModal", $button.text(), AS.renderTemplate("modal_content_loading_template"), 'full', {keyboard: false}, $button); - $(document).triggerHandler("rdeload.aspace", [$node, $modal]); + $(document).triggerHandler("rdeload.aspace", [$node.data('uri'), $modal]); }); }); diff --git a/frontend/app/assets/javascripts/tree.js.erb b/frontend/app/assets/javascripts/tree.js.erb index 07e994e2cf..9cd20e8e01 100644 --- a/frontend/app/assets/javascripts/tree.js.erb +++ b/frontend/app/assets/javascripts/tree.js.erb @@ -1,1418 +1,55 @@ -//= require jquery.cookie -//= require jquery.ba-hashchange -//= require jstree -//= require jstree.primary_selected -//= require jstree.select_limit +//= require ajaxtree +//= require tree_renderers +//= require tree_toolbar +//= require tree_resizer +//= require largetree +//= require largetree_dragdrop -$(function() { - var tree; - var $tree = $("#archives_tree"), - $container = $("#object_container"), - $toolbar = $("#archives_tree_toolbar"); +(function (exports) { + "use strict"; - if($tree.length === 0) { - return; - } - - var moved_nodes = []; - var move_timeout = null; - - var desired_children = []; // this is where we'll store what we want/think the children will be ordered. - - // use of loadedrecordform event to ok tree tweaks - $(document).bind("loadedrecordform.aspace", function() { - if (!config.read_only) { - AS.tree_data.moveable = true; - } - }); - - if ($tree.length < 1) { - return; - } - - var path_to_node; - - var config = { - root_object_id: $tree.data("root-id"), - root: $tree.data("root"), - root_node_type: $tree.data("root-node-type"), - read_only: $(".archives-tree").data("read-only"), - rules: $(".archives-tree").data("rules") - }; - - var initForm = function(html) { - $container.html(html); - - var $form = $("form", $container); - - // if (location.hash !== "#tree::new") { - $form.data("form_changed", false); - $(".btn-primary", $form).attr("disabled", "disabled"); - $(".btn-cancel", $form).attr("disabled", "disabled"); - // } else { - // $form.data("form_changed", true); - // } - - - $form.on("formchanged.aspace", function() { - - $(".btn-primary", $form).removeAttr("disabled"); - $(".btn-cancel", $form).removeAttr("disabled"); - }); - - $form.on("formreverted.aspace", function(event, data) { - - if ($("form", $container).data("form_changed")) { - - var p = confirmChanges(tree.get_primary_selected()); - p.done(function(proceed) { - if(proceed) { - $(window).trigger('hashchange'); - $.scrollTo("header") - } - return; - }); - } - - }); - - - $(".btn-plus-one", $form).click( function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - var createPlusOne = function(event, data) { - if (data.success) { - $(".archives-tree-container .add-sibling").trigger("click"); - } - }; - - $form.data("createPlusOne", true); - - $container.one("submitted", createPlusOne); - $form.triggerHandler("submit"); - }); - - - // Move the loadedrecordform.aspace event to be before - // the ajaxForm is attached, so events (like the session check) - // can be bound to the form first. - // See form.js - $(document).triggerHandler("loadedrecordform.aspace", [$container]); - - - // We handle form reverts ourselves. - // There's a revert button above... - $(".record-toolbar .revert-changes .btn", $form).off().click(function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - $form.trigger("formreverted.aspace"); - - }); - - // ..and another down below - $(".btn-cancel", $form) - .html(AS.renderTemplate("tree_revert_changes_label_template")) - .off() - .click(function(event) { - // scroll back to the top - - event.preventDefault(); - event.stopImmediatePropagation(); - $form.trigger("formreverted.aspace"); - }); - - - $form.ajaxForm({ - data: { - inline: true - }, - beforeSubmit: function(arr, $form) { - $(".btn-primary", $form).attr("disabled","disabled"); - - if ($form.data("createPlusOne")) { - arr.push({ - name: "plus_one", - value: "true", - required: false, - type: "text" - }); - } - }, - success: function(response, status, xhr) { - var $form = initForm(response); - - if ($form.data('formErrors')) { - $container.triggerHandler("submitted", {success: false}); - $form.data("form_changed", true); - $(".btn-primary, .btn-cancel", $form).removeAttr("disabled"); - } else { - $form.data("form_changed", false); - - $container.triggerHandler("submitted", {success: true}); - } - - if ( $form.data('"update-monitor-paused"') ) { - $form.data("update-monitor-paused", false); - } - - // scroll back to the top - $.scrollTo("header"); - }, - error: function(obj, errorText, errorDesc) { - $container.prepend("<div class='alert alert-error'><p>An error occurred saving this record.</p><pre>"+errorDesc+"</pre></div>"); - $container.triggerHandler("submitted", {success: false}); - $(".btn-primary", $form).removeAttr("disabled"); - } - }); - - AS.resetScrollSpy(); - - return $form; - }; - - var insertLoadingMessage = function() { - var loadingMsgEl = $(AS.renderTemplate("tree_loading_notice_template")); - loadingMsgEl.hide(); - $container.prepend(loadingMsgEl); - loadingMsgEl.fadeIn(); - $(":input", $container).attr("disabled","disabled"); - }; - - - var insertTreeOverlay = function() { - $("#archives_tree_overlay").height('100%'); - } - - var removeTreeOverlay = function() { - $("#archives_tree_overlay").height('0%'); - } - - var toggleTreeSpinner = function(){ - $(".archives-tree-container .spinner").toggle(); - } - - var extractNode = function(data, url) { - var node_uri = url.match(/node_uri=([^&]*)/gm)[0].replace("node_uri=", "") - - if (node_uri === 'root') { - return data; - } else if (node_uri === data.record_uri) { - return (typeof(data.children) === 'object' && data.children.length > 0) ? data.children : [] - } else if (_.isUndefined(data)) { - return false; - } else { - return extractNode(_.find(data.children, function(child) { - return (typeof(child.children) === 'object' && child.children.length > 0); - }), url); - } - }; - - - var getPrimarySelectedNode = function() { - return tree.get_primary_selected_dom(); - }; - - var getCurrentPath = function() { - return tree.get_path(tree.get_primary_selected(), false, true); - }; - - var getSelectedNodes = function(full, sorted) { - full = full || false; - var nodes = tree.get_selected(full); - - if (full && sorted) { - return _.sortBy(nodes, function(node) { - return getNodePosition(node); - }); - } else { - return nodes; - } - }; - - var getNodePosition = function(obj) { - obj = _.isString(obj) ? tree.get_node(obj) : obj - - if (!obj.parent) { - return false; - } - - return _(tree.get_node(obj.parent).children) - .findIndex(function(childId) { - return childId === obj.id; - }); - }; - - var removeLoadingMessage = function() { - var loadingMsgEl = $('.tree-loading-notice') - loadingMsgEl.fadeOut(); - loadingMsgEl.remove(); - $(":input", $container).removeAttr("disabled"); + var renderers = { + resource: new ResourceRenderer(), + digital_object: new DigitalObjectRenderer(), + classification: new ClassificationRenderer(), }; - var isLastSibling = function(node) { - node = _.isString(node) ? tree.get_node(node) : node - return getNodePosition(node) > (tree.get_node(node.parent).children.length -2); - } + function Tree(datasource_url, tree_container, form_container, toolbar_container, root_uri, read_only, root_record_type) { + var self = this; - var previousSibling = function(node) { - return tree.get_node(node.parent).children[getNodePosition(node) - 1] - } + self.datasource = new TreeDataSource(datasource_url); - var nextSibling = function(node) { - return tree.get_node(node.parent).children[getNodePosition(node) + 1] - } + var tree_renderer = renderers[root_record_type]; - var loadPaneForNode = function(node) { - var nodeEl = tree.get_node(node, true); + self.toolbar_renderer = new TreeToolbarRenderer(self, toolbar_container); - insertLoadingMessage(); + self.root_record_type = root_record_type; - if (config.read_only) { + self.large_tree = new LargeTree(self.datasource, + tree_container, + root_uri, + read_only, + tree_renderer, + function() { + self.ajax_tree = new AjaxTree(self, form_container); + self.resizer = new TreeResizer(self, tree_container); + }, + function(node, tree) { + self.toolbar_renderer.render(node); + }); - var params = {}; - params.inline = true; - params[config.root_node_type+"_id"] = config.root_object_id; - $.ajax({ - url: APP_PATH+node.type+"s/"+node.id.replace(/.*_/, ''), - type: 'GET', - data: params, - success: function(html) { - $container.html(html); - AS.resetScrollSpy(); - $(document).trigger("loadedrecordform.aspace", [$container]); - }, - error: function(obj, errorText, errorDesc) { - $container.html("<div class='alert alert-error'><p>An error occurred loading this form.</p><pre>"+errorDesc+"</pre></div>"); - } - }); - - return; - } - - if (node.id === "new") { - - var data = { - inline: true - }; - data[config.root_node_type + "_id"] = config.root_object_id; - - var $parentNodeEl = nodeEl.parents("li:first"); - if ($parentNodeEl.attr("rel") === nodeEl.attr("rel")) { - data[$parentNodeEl.attr("rel") + "_id"] = $parentNodeEl.data("id"); - } - - if (nodeEl.data("params")) { - data = $.extend({}, data, nodeEl.data("params")); - } - - $.ajax({ - url: APP_PATH + node.type + "s/new", - data: data, - type: "GET", - success: function(html) { - initForm(html); -// $("form", $container).triggerHandler("formchanged.aspace"); - }, - error: function() { - $container.html("<div class='alert alert-error'><p>An error occurred loading this form.</p><pre>"+errorDesc+"</pre></div>"); - } - }); - } else if (node.type) { - $.ajax({ - url: APP_PATH+node.type+"s/"+node.id.replace(/.*_/,'')+"/edit?inline=true", - success: function(html) { - initForm(html); - }, - error: function(obj, errorText, errorDesc) { - $container.html("<div class='alert alert-error'><p>An error occurred loading this record.</p><pre>"+errorDesc+"</pre></div>"); + if (!read_only) { + self.dragdrop = self.large_tree.addPlugin(new LargeTreeDragDrop()); } - }); - } - }; - - - var renderTreeNodeNavigation = function(event) { - var node = tree.get_primary_selected(true); - - var data = { config: config } - var indexOfCurrentNode = getNodePosition(node); - - if (indexOfCurrentNode && indexOfCurrentNode !== 0) { - data.previous = previousSibling(node); - } else if (node.parent) { - data.previous = node.parent - } - - if (node.children && node.children.length > 0) { - data.next = node.children[0]; - } else if (!isLastSibling(node)) { - data.next = nextSibling(node); - } - - $(".btn-toolbar", $toolbar).append(AS.renderTemplate("tree_nodenavigation_toolbar_template", data)); - }; - - - var loadTreeActionsForNode = function(node) { - var node = tree.get_node(node); - var nodeEl = tree.get_node(node, true); - // render tree toolbar - $toolbar.html(AS.renderTemplate("tree_toolbar_template")); - renderTreeNodeNavigation(); - if (config.read_only !== true && nodeEl.attr("id") != "new") { - var data_for_toolbar = { - config: config, - rules: config.rules[node.type], - node_id: node.id.replace(/.*_/, ''), - root_object_id: config.root_object_id, - up_level: node.parents.length > 2 - }; - if (node.parent) { - var parent = tree.get_node(tree.get_parent(node)); - data_for_toolbar.parent_id = parent.id; - data_for_toolbar.parent = parent; - data_for_toolbar.is_first = parent.children[0] === node.id; - data_for_toolbar.is_last = _.last(parent.children) === node.id; - data_for_toolbar.siblings = _(parent.children) - .filter(function(sibling) { - return sibling != node.id; - }) - .map(function(sibling) { - sibling = tree.get_node(sibling); - return { - id: sibling.id, - title: sibling.original.title - }; - }).value(); - } - - var $toolbarActions = $(AS.renderTemplate("tree_nodeactions_toolbar_template", data_for_toolbar)); - - $(".btn-toolbar", $toolbar).append($toolbarActions); - - // init "close record" button - var $closeRecordButton = $(AS.renderTemplate("tree_finish_action_template", data_for_toolbar)); - - // check before closing a dirty form - $('.btn-success', $closeRecordButton).click(function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - var location = $(this).attr('href'); - if ($("form", $container).data("form_changed")) { - var p = confirmChanges(tree.get_primary_selected_dom()); - p.done(function(proceed) { - if (proceed) { - window.location = location; - } - }); - } else { - window.location = location; - } - }); - - $(document).on('formclosed.aspace',function() { - $closeRecordButton.trigger("click"); - }); - - $(".btn-toolbar", $toolbar).append($closeRecordButton); - - $('a', $toolbar).on("focus", function() { - if ($(this).parents("li.dropdown-submenu").length) { - $('.dropdown-menu', $(this).parent()).show(); - } else { - $(".dropdown-submenu .dropdown-menu", $(this).parents(".nav")).css("display", ""); - } - }); - - $('.dropdown-submenu > a', $toolbar).on("keyup", function(event) { - // right arrow focuses submenu - if (event.keyCode === 39) { - $('.dropdown-menu a:first', $(this).parent()).focus(); - } - }); - $('.dropdown-submenu > .dropdown-menu > li > a', $toolbar).on("keyup", function() { - // left arrow focuses parent menu - if (event.keyCode === 37) { - $("> a", $(this).parents(".dropdown-submenu:first")).focus(); - } - }); - - // init the cut and paste actions - if ($tree.data("clipboard")) { - // can't allow paste if clipped nodes are in current path - if (!_.intersection(getCurrentPath(), $tree.data("clipboard")).length) { - $(".paste-node", $toolbar).removeAttr("disabled").removeClass("disabled"); - } - } - - $(".cut-node", $toolbar).removeAttr("disabled").removeClass("disabled"); - - // toggle action disabled/enabled based on status of tree - enableDisableToolbarActions(); - - // init any widgets in the newly rendered toolbar - $(document).trigger("loadedrecordform.aspace", [$toolbar]); - } - }; - - - var setHashForNode = function(node_id) { - - if (node_id.indexOf("tree::") < 0) { - tree_node_id = "tree::"+node_id; - } else { tree_node_id = node_id } - - // if the hash is empty or is the same, this is the first time the page has been loaded...so we don't need to - // retrigger the onHashChange callback. or is its #resource_3 and the hash is being changed to - // tree::resource_3. ugh. - - if ( location.hash.length < 1 || location.hash === "#" + node_id || location.hash === node_id || location.hash === tree_node_id ) { - changeHashSilently(tree_node_id); - } else { - location.hash = tree_node_id; - } - }; - - - var changeHashSilently = function(newHash) { - $(window).data("ignore-hashchange", true); - $('.locked').removeClass('locked'); - location.hash = newHash; - }; - - - var onHashChange = function(){ - var currentlyFocusedSelector; - $('.locked').removeClass('locked'); - // if a toolbar action is clicked, let's refocus it - // up re-render (it will be replaced with a new toolbar - // for the selected node) - if ($(":focus").length > 0 && $(":focus").closest($toolbar).length > 0) { - currentlyFocusedSelector = $(":focus").attr("class"); - } - - if ($(window).data("ignore-hashchange")) { - $(window).data("ignore-hashchange", false); - return; - } - - if (!location.hash || location.hash.indexOf("tree::") === -1) { - return; - } - - var id_from_hash = location.hash.replace("#tree::", ""); - - tree.set_primary_selected(id_from_hash, function(node) { - if (!node.state.opened) { - tree.open_node(node, function(node) { - loadPaneForNode(node); - loadTreeActionsForNode(node); - }); - } else { - loadPaneForNode(node); - loadTreeActionsForNode(node); - } - }); - - if (currentlyFocusedSelector) { - var $target = $("[class='"+currentlyFocusedSelector+"']"); - - if ($target.is("[disabled]")) { - $(".btn:not([disabled]):first", $toolbar).focus(); - } else { - $("[class='"+currentlyFocusedSelector+"']").focus(); - } - } - }; - - - var addnewNode = function(parent, newNodeType, newNodeConfig, new_object_params) { - var newNode = { - id: "new", - node_type: newNodeType, - type: newNodeType, - text: newNodeConfig.record_label, - icon: 'glyphicon glyphicon-asterisk', - a_attr: { - "href": "#new" - }, - li_attr: { - "rel": newNodeType, - "class": "new", - "data-params": new_object_params ? new_object_params : "" - } }; - var node_id = tree.create_node(parent, newNode, "last"); - - setHashForNode("new"); - }; - - - var resizeArchivesTree = function() { - var height = $("#archives_tree").parent().height() - $toolbar.outerHeight() - 21; - $("#archives_tree").height(height); - }; - - - var enableDisableToolbarActions = function() { - if (tree.get_primary_selected(true).parents.length < 3) { - $(".move-node-up-level", $toolbar).attr("disabled", "disabled").addClass("disabled"); - } - }; - - - var addTreeEventBindings = function() { - - $(".archives-tree-container").on("click", ".add-child", function() { - if (!tree.get_primary_selected()) - return; - - var parent = tree.get_primary_selected(); - var parentType = tree.get_primary_selected(true).original.type; - - addnewNode(parent, $(this).attr("rel"), config.rules[parentType].can_add_child); - }); - - $(".archives-tree-container").on("click", ".add-sibling", function() { - if (!tree.get_primary_selected()) - return; - - var parent = tree.get_primary_selected(true).parent; - var type = tree.get_primary_selected(true).type; - - var parentType = tree.get_node(parent).type; - - addnewNode(parent, type, config.rules[parentType].can_add_child); - }); - - $container.on("click", ".btn-cancel", function(event) { - - event.preventDefault(); - event.stopImmediatePropagation(); - if ($(this).attr("disabled")) { - return; - } - - if (getPrimarySelectedNode().attr("id") === "new") { - setHashForNode(getPrimarySelectedNode().parents("li:first").attr("id")); - } else { - loadPaneForNode(tree.get_primary_selected(true)); - } - }); - - $(".archives-tree-container").on("click", ".expand-tree .btn", function() { - $(".archives-tree-container").addClass("expanded"); - $(".archives-tree-container").animate({ - width: $(".archives-tree-container").parents(".container:first").width()-5 - }, 500); - }); - - $(".archives-tree-container").on("click", ".retract-tree .btn", function() { - $(".archives-tree-container").animate({ - width: $(".archives-tree-container").parent().width() - }, 500, function() { - $(".archives-tree-container").removeClass("expanded"); - $(".archives-tree-container").css("width", "auto"); - }); - }); - - $(".archives-tree-container").on("click", ".move-node", function(event) { - event.preventDefault(); - event.stopPropagation(); - - var nodesToMove; - var leadNode; - - var getNodesToMove = function() { - nodesToMove = getSelectedNodes(true, true); - leadNode = nodesToMove[0]; - } - getNodesToMove(); - - switch($(this).attr('rel')) { - - case "down": - if ( leadNode === undefined ) { getNodesToMove(); } - var targetParent = leadNode.parent - var targetPosition = getNodePosition(leadNode) + nodesToMove.length + 1; - - tree.move_node(nodesToMove, targetParent, targetPosition); - break; - - case "up": - - if ( leadNode === undefined ) { getNodesToMove(); } - var targetParent = leadNode.parent - var targetPosition = getNodePosition(leadNode) - 1; - - if (targetPosition > -1) { - tree.move_node(nodesToMove, targetParent, targetPosition); - } - break; - - case "down-into": - - var targetParent = $(this).data("target-node-id"); - tree.move_node(nodesToMove, targetParent, 0); - tree.open_node(targetParent); - break; - - case "up-level": - - var targetParent = tree - .get_path(leadNode, false, true).slice(-3, -2); - tree.move_node(nodesToMove, targetParent, 0); - - break; - } - }); - - - // TRANSFER STUFF - $(".archives-tree-container").on("click", ".transfer-node", function(event) { - if ($(".tree-transfer-form", ".archives-tree-container")[0].style.display === "block") { - - $(".tree-transfer-form", ".archives-tree-container").css("display", ""); - $(".missing-ref-message", ".archives-tree-container .tree-transfer-form form").hide(); - $(".token-input-dropdown").hide(); - } else { - $(".tree-transfer-form", ".archives-tree-container").css("display", "block"); - $(".tree-transfer-form form", ".archives-tree-container").unbind("submit").submit(function(event) { - - - if ($(this).serializeObject()['transfer[ref]']) { - // continue with the POST - return; - } else { - event.stopPropagation(); - event.preventDefault(); - - $(".missing-ref-message", ".archives-tree-container .tree-transfer-form form").show(); - return true; - } - }); - setTimeout(function() { - $("#token-input-transfer_ref_", ".archives-tree-container").focus(); - }); - } - }); - $(".archives-tree-container").on("click", ".tree-transfer-form :input", function(event) { - event.stopPropagation(); - }); - $(".archives-tree-container").on("click", ".tree-transfer-form .dropdown-toggle", function(event) { - event.stopPropagation(); - $(this).parent().toggleClass("open"); - }); - $(".archives-tree-container").on("click", ".tree-transfer-form .btn-cancel", function(event) { - $(".tree-transfer-form", ".archives-tree-container").css("display", ""); - $(".transfer-node", ".archives-tree-container").parent().removeClass("open"); - }); - - // Cut and Paste - $(".archives-tree-container").on("click", ".cut-node", function(event) { - event.preventDefault(); - if ($(this).hasClass("disabled")) { - return; - } - - _.forEach(tree.get_selected(), function(selected) { - tree.get_node(selected, true).addClass("cut-to-clipboard"); - }); - - $(".cut-to-clipboard", $tree).removeClass("cut-to-clipboard"); - - $tree.data("clipboard", tree.get_selected()); - $(this).addClass("disabled").addClass("btn-success"); - }).on("click", ".paste-node", function(event) { - event.preventDefault(); - if ($(this).hasClass("disabled") || !$tree.data("clipboard")) { - return; - } - - var target = tree.get_primary_selected(); - - tree.move_node($tree.data("clipboard"), target); - - - $tree.data("clipboard", null); - $(this).addClass("disabled"); - $(".cut-to-clipboard", $tree).removeClass("cut-to-clipboard"); - - - }); - - // Rapid Data Entry - $(".archives-tree-container").on("click", ".add-children", function() { - var $selected = getPrimarySelectedNode(); - if ($selected.length === 0) { - return; - } - - $(document).triggerHandler("rdeshow.aspace", [$selected, $(this)]); - }); - - $(".archives-tree-container").on("click", '.refresh-tree', function(event) { - event.preventDefault(); - event.stopImmediatePropagation(); - tree.refresh(); - }); - - $(window).hashchange(onHashChange); - - - $("#archives_tree").scroll(function() { - if ($(this).scrollTop() === 0) { - $(this).removeClass("overflow"); - } else { - $(this).addClass("overflow"); - } - }); - - if (AS.prefixed_cookie("archives-tree-container::height")) { - $(".archives-tree-container").height(AS.prefixed_cookie("archives-tree-container::height")); - } else { - $(".archives-tree-container").height(AS.DEFAULT_TREE_PANE_HEIGHT); - } - - $(".archives-tree-container").resizable({ - handles: "s", - minHeight: 80, - resize: function(event, ui) { - AS.prefixed_cookie("archives-tree-container::height", ui.size.height); - resizeArchivesTree(); - $(window).triggerHandler("resize.tree"); - } - }); - - }; - - - // In JSTree you say where, relative to the pre-op tree, - // you want moving nodes to go, and it takes care of the rest. - // In AS Backend you say where you want the moved nodes to - // be in the tree after the move operation, and it takes care of the rest. - // When JSTree is done it sends us this data and we need to translate. - // - // If the nodes moved 'down' (higher in the index), - // the position of the first node in the data is: - // the former position of the node it bumped lower. - // To translate this for ASpace: - // leader's position + the gap created by the splice - 1 for - // the node that slid lower in the index. - - var moveNodes = function(collectedMoveData) { - - var targetPosition; - - // let's lock down the tree - insertTreeOverlay(); - toggleTreeSpinner(); - - // now let's lock down the form - $(document).triggerHandler("lockform.aspace", [$container]); - - // if we've been given an array of string ids, that's the order we want them in. - if ( typeof collectedMoveData[0] === "string" ) { - collectedMoveData = _.map(collectedMoveData, function(node_id) { - return tree.get_node(node_id); - } ); - // we're just sticking this in there as we see it. - targetPosition = 0; - } else { - // alright, we've got selected objects. sort by position under the new parent - collectedMoveData = _.sortBy(collectedMoveData, function(data) { - return data.position; - }); - - var leader = collectedMoveData[0]; - targetPosition = leader.position; - - // If we have moved a node downwards in the list... Since the - // list will shift up as the node is plucked out, we need to - // adjust the target index for this. - // TO DO: if the position is the last in the list, this will screw it - // up. - if ((leader.parent === leader.old_parent) && - (leader.position > leader.old_position)) { - targetPosition -= collectedMoveData.length - 1; - } - } - - var targetNode = tree.get_node(collectedMoveData[0].parent); - var targetNodeEl = tree.get_node(targetNode, true); - - //sanity check - shouldn't ever happen as there should always be just one - //parent. - if (_.uniq(_.map(collectedMoveData, function(md) { - return md.parent - })).length > 1) { - return false; - } - - var urisOfNodesToMove = _.map(collectedMoveData, function(data) { - if ( _.has(data, 'node') ) { - return data.node.original.record_uri; - } else { return data.original.record_uri; } - }); - - var data_for_post = { - children: urisOfNodesToMove, - index: targetPosition - }; - $.ajax({ - url: APP_PATH+targetNode.type+"s/"+targetNode.id.replace(/.*_/, '')+"/accept_children", - type: "POST", - data: data_for_post, - success: function(data, status, jqXHR) { - // if we get back position from the backend that's different than - // the index from the frontend, we might have a problem so let's refresh - // the tree - if ( data_for_post.index !== data.position ){ - tree.refresh(); - AS.openQuickModal(AS.renderTemplate("tree_unable_to_move_message_template"), jqXHR.responseText); - } else { - - tree.refresh_primary_selected(); - tree.primary(function(node) { - var nodeEl = tree.get_node(node, true); - // If the reparented record's form is open, update its hidden field. - if (urisOfNodesToMove.indexOf(node.original.record_uri) >= 0) { - - var hiddenInput = $("input.hidden-parent-uri", $container); - - if (targetNode.type === node.type) { - hiddenInput.attr('name', node.type + '[parent][ref]'); - hiddenInput.val(targetNode.original.record_uri); - } else { - hiddenInput.attr('name', node.type + '[parent]'); - hiddenInput.val(null); - } - } - - // Always update the currently selected node's position.. it may have changed! - $("#"+nodeEl.attr("rel")+"_position_", $container).val(nodeEl.index()); - - desired_children = targetNode.children; - - var selected = tree.get_selected(); - // lets give the thing at least one second so we don't cause any seizures with flashing annimation. - setTimeout( function() { - // desired_children = []; - removeTreeOverlay(); - toggleTreeSpinner(); - tree.select_node(selected); - }, 1000); - - // now let's unlock da the form - - // trying this again. if the target node is the root, - // we need to get the parent and then refresh it in - // a slightly different way... - if ( targetNode.parent === "#" ) { - parent = tree.get_node(tree.get_parent(desired_children[0])); - tree.refresh_node(parent); - } else { - // otherwise, just refresh that level... - tree.refresh_node(targetNode); - } - - $(document).triggerHandler("unlockform.aspace", [$container]); - - - }); - - } - }, - error: function(jqXHR, textStatus, errorThrown) { - // Reset the tree. - tree.refresh(); - - // show a modal message - AS.openQuickModal(AS.renderTemplate("tree_unable_to_move_message_template")); - } - }); - }; - - - var confirmChanges = function(target) { - var node = tree.get_node(target); - var targetNodeEl = tree.get_node(node, true); - - var d = $.Deferred(); - - var parentIdFornew; - if (targetNodeEl.attr("id") === "new") { - parentIdFornew = targetNodeEl.parents("li:first").attr("id"); - } - - // open save your changes modal - AS.openCustomModal("saveYourChangesModal", "Save Your Changes", AS.renderTemplate("save_changes_modal_template")); - - $("#saveChangesButton", "#saveYourChangesModal").on("click", function() { - var $form = $("form", $container); - - $form.triggerHandler("submit"); - - var onSubmitSuccess = function() { - $form.data("form_changed", false); - setHashForNode(node.id); - $("#saveYourChangesModal").modal("hide"); - d.resolve(true); - }; - - var onSubmitError = function() { - $("#saveYourChangesModal").modal("hide"); - d.resolve(false); - }; - - $container.one("submitted", function(event, data) { - if (data.success) { - onSubmitSuccess(); - } else { - onSubmitError(); - } - }); - }); - - $("#dismissChangesButton", "#saveYourChangesModal").on("click", function() { - $("form", $container).data("form_changed", false); - if (targetNodeEl.attr("id") != "new") { - $tree.jstree("delete_node", $('#new', $tree)); - } -// setHashForNode(targetNodeEl.attr("id")); - $("#saveYourChangesModal").modal("hide"); - d.resolve(true); - }); - - $(".btn-cancel", "#saveYourChangesModal").on("click", function() { - if (targetNodeEl.attr("id") === "new") { - $tree.jstree("delete_node", $('#new', $tree)); - } - d.resolve(false); - }); - - return d.promise(); - }; - - - var onSelectNode = function() { - - $(".selected", $tree).removeClass("selected"); - - if (tree.get_selected().length > 1) { - $.each(tree.get_selected(), function(i, node_id) { - $nodeEl = tree.get_node(node_id, true); - $nodeEl.addClass("selected"); - $('.selected-order', $nodeEl).html( i + 1 ).show(); - }); - - $tree.trigger("treemultipleselected.aspace"); - } else if (tree.get_selected().length === 1) { - tree.set_primary_selected(tree.get_selected()[0]); - - var obj = tree.get_selected(true)[0]; - - //open the node if it's closed - if (!obj.state.opened) { - tree.open_node(obj); - } - - //wipe out any "new" nodes if they're - //still hanging around - if (obj.id != "new") { - tree.delete_node("new"); - } - - setHashForNode(tree.get_primary_selected()); - - $nodeEl = tree.get_primary_selected_dom(); - $nodeEl.addClass("selected"); - - $('.selected-order').empty(); - $('.selected-order:first', $nodeEl).html( 1 ).show(); - - tree.hover_node(tree.get_primary_selected()); - $tree.trigger("treesingleselected.aspace"); - } - } - - - var initiallyOpenedNodes = function() { - if (path_to_node) { - return path_to_node; - } - if (location.hash) { - var hash_id = location.hash.replace("tree::",""); - if (AS.tree_data.hasOwnProperty(hash_id.replace("#", ""))) { - path_to_node = []; - var node_id = hash_id.replace("#", ""); - while(true) { - var node = AS.tree_data[node_id]; - path_to_node.push(node_id); - if (node.hasOwnProperty("parent")) { - node_id = node.parent; - } else { - path_to_node = path_to_node.reverse(); - path_to_node.pop(); - return path_to_node; - } - } - } - } - return []; - }; - - var renderNodeText = function(data) { - if (_.isArray(data)) { - _.forEach(data, function(e) { - renderNodeText(e); - }); - } else { - data.text = AS.renderTemplate("tree_node_" + data.node_type + "_template", {node: data}); - - if(_.has(data, "children") && _.isArray(data.children)) { - renderNodeText(data.children) ; - } - } - } - - - var initTree = function(onTreeLoadCallback) { - AS.tree_data = { - moveable: false + Tree.prototype.current = function() { + return $('.current', this.large_tree.elt); }; - $tree.jstree({ - core: { - data: function(obj, cb) { - var url = $tree.data("tree-url"); - if (obj.id === '#') { - url += "?node_uri=root&hash=" + location.hash.replace("#", ""); - } else { - url += "?node_uri=" + obj.original.record_uri; - } - - $.ajax(url, { - success: function(data) { - _.tap(extractNode(data, url), function(nodeAppendees) { - renderNodeText(nodeAppendees); - cb(nodeAppendees); - }); - } - }); - }, - check_callback: function(operation, node, node_parent, node_position) { - switch(operation) { - case "create_node": - return AS.tree_data.moveable; - case "rename_node": - return AS.tree_data.moveable; - case "move_node": - if (AS.tree_data.moveable) { - // only allow drop on root node as child - // this will disable the move/drop to a sibling of the root node - if (node_parent.parent == null) { - return false; - } else { - return true; - } - } else { - return false; - } - case "delete_node": - return node.id === "new" - default: - return AS.tree_data.moveable; - } - }, - animation: 0, - html_titles: true - }, - types: { - "default": { - "icon" : "glyphicon glyphicon-file" - }, - "archival_object": { - "icon" : "glyphicon glyphicon-file" - }, - "resource": { - "icon" : "glyphicon glyphicon-list-alt" - }, - "digital_object_component": { - "icon" : "asicon icon-digital_object_component" - }, - "digital_object": { - "icon" : "asicon icon-digital_object" - }, - "classification": { - "icon" : "glyphicon glyphicon-file" - }, - "classification_term": { - "icon" : "glyphicon glyphicon-file" - } - }, - themes: { - name: "default", - url: false, - dots: true - }, - dnd: { - drop_target : false, - drag_target : false - }, - ui: { - selected_parent_close: false, - selected_parent_open: true, - select_limit: -1, - // if OS X, use meta for single select (as ctrl used for menu) - select_multiple_modifier: (navigator.appVersion.indexOf("Mac") != -1) ? "meta" : "ctrl", - disable_selecting_children: true - }, - crrm: { - move: { - check_move: function (m) { - if ( config.read_only ) { - return false; - } - // can't move top level parent - if ($(m.o[0]).hasClass(config.root_node_type)) { - return false; - } - - // can't move to above the root child node - if ($(m.np[0]).hasClass("archives-tree")) { - return false; - } - - return true; - } - } - }, - - hotkeys: { - "up": false, - "down": false, - "ctrl+up": function() { - $(".move-node-up",$toolbar).trigger("click"); - }, - "shift+up": false, - "ctrl+down": function() { - $(".move-node-down",$toolbar).trigger("click"); - }, - "shift+down": false, - "ctrl+left": function() { - $(".move-node-up-level",$toolbar).trigger("click"); - }, - "shift+left": false, - "ctrl+right": function() { - $($(".move-node-down-into",$toolbar)[getPrimarySelectedNode().index()]).trigger("click"); - }, - "shift+right": false, - "space": false, - "ctrl+space": false, - "shift+space": false, - "f2": false, - "del": false, - "return": function() { - $('.jstree-hovered', $tree).trigger("click"); - } - }, - plugins: [ "select_limit", "primary_selected", "types", "themes", "ui", "html_data", "crrm", "dnd", "hotkeys"] - }).one("loaded.jstree", function() { - - tree = $tree.jstree(true); - AS._tree = tree; //just for browser debugging - - if (location.hash) { - var node_id = location.hash.replace("#tree::",""); - tree.select_node(node_id); - } - - if (!tree.get_primary_selected()){ - if(tree.get_selected().length > 0) { - tree.set_primary_selected(tree.get_selected()[0]); - } else { - tree.set_primary_selected('#'); - } - } - - tree.primary(function(node) { - tree.open_node(node); - loadTreeActionsForNode(node); - }); - - onTreeLoadCallback(); - }).bind("select_node.jstree", function (event, data) { - // if the form is dirty AND the selected node isn't the primary - // then do a modal check - if ($("form", $container).data("form_changed") && data.node.id != tree.get_primary_selected()) { - var p = confirmChanges(tree.get_primary_selected()); - p.done(function(proceed) { - if (proceed) { - onSelectNode(); - } else { - tree.refresh_primary_selected(); - } - }); - p.fail(function(err) { - console.log(err); - }); - } else { - onSelectNode(); - } - - }).bind("deselect_node.jstree", function (event, data) { - var $node = $tree.jstree("get_node", data.node, true); - if ($node.hasClass("primary-selected")) { - // we don't want to allow deselection of the primary selected node - $tree.jstree("select_node", $node); - } else { - $node.removeClass("selected"); - } - - }).bind("move_node.jstree", function(event, data) { - if (move_timeout) { clearTimeout(move_timeout); } - moved_nodes.push(data); - move_timeout = setTimeout(function() { - moveNodes(moved_nodes); - // lets clear out the moved_nodes - - moved_nodes = []; - onSelectNode(); - }, 50); - - }).bind("refreshtreenode.aspace", function(event, data) { - - AS.tree_data.moveable = true; - - var a_title = data.title || data.text; - - data.text = AS.renderTemplate("tree_node_" + data.node_type + "_template", {node: data}); - - data.type = data.node_type - - var nodeId = data.jsonmodel_type+"_"+data.id; - - if (data.replace_new_node) { - // drop the 'new' node and replace with a record node - - // This was added to make a test pass - // but something is amiss somewhere - if (data.uri && !data.record_uri) { - data.record_uri = data.uri; - } - - - var isSelected = ("new" === tree.get_primary_selected()); - - var anon_node = tree.get_node("new"); - var parent = anon_node.parent; - - data.a_attr = { - "href": "#" + nodeId, - "title": a_title - } - data.li_attr = { - "data-id": data.id, - "data-uri": data.uri, - id: nodeId, - rel: data.node_type - } - data.id = nodeId; - - tree.delete_node(anon_node); - - var node = tree.create_node(parent, data, "last", function(node) { - - if (isSelected) { - tree.set_primary_selected(node); - } - - changeHashSilently("#tree::"+nodeId); - loadTreeActionsForNode(node); - }) - - } else { - // rename the existing node - tree.rename_node(nodeId, data.text); - $('a', tree.get_node(nodeId, true)).attr('title', a_title); - tree.refresh_primary_selected(); - } - - }).on("after_open.jstree", function(event, obj) { - - // make sure selected styles are there if the node was lazy-loaded - var nodeEl = tree.get_primary_selected_dom(); - if (nodeEl && nodeEl.length) { - nodeEl.addClass("primary-selected"); - } - - }).on("click", ".jstree-leaf a", function(event) { - // only focus the tree node if the event originates - // from a mouse click (otherwise it will be triggered - // by another event). - if (typeof event.isTrigger == "undefined") { - $(this).focus(); - } - }); - - }; - - initTree(function() { - resizeArchivesTree(); - tree.primary(function(node) { - // if the hash is not set, the loadPaneForNode will set it. but we do not - // want to retrigger the call for the record. - if ( location.hash.length === 0 ) { - $(window).data("ignore-hashchange", true); - } - loadPaneForNode(node); - setTimeout(function(){ - $tree.scrollTo($("#" + node.id), 0, { - offsetTop: $tree.height() / 2 - }); - },0); - }); - }); - - addTreeEventBindings(); - - - AS.refreshTreeNode = function(data) { - $tree.triggerHandler("refreshtreenode.aspace", [data]); - }; - - - $tree.bind("refresh_node.jstree", function(event, data) { - - if ( desired_children.length == 0 ) { return; } - - /* For some unknown reason, data.node doesn't reliably have its children - populated. Just use its ID to get back the full node from the jstree - instance. */ - var refreshed_node = data.instance.get_node(data.node.id) - - happy_family = desired_children.length == refreshed_node.children.length && desired_children.every(function(element, index) { - return element === refreshed_node.children[index]; - }); - if ( happy_family === false ) { - AS.openQuickModal(AS.renderTemplate("tree_move_ordering_problem_message_template"), desired_children); - } - // we're good! - desired_children = []; - }); - // this sets the tree into readonly mode but does not 'lock' it. - $(document).bind("readonlytree.aspace", function(event) { - AS.tree_data.moveable = false; - }); - - $(document).bind("formreverted.aspace", function(event) { - AS.tree_data.moveable = true; - $('.refresh-tree').click(); - }); -}); + exports.Tree = Tree; +}(window)); diff --git a/frontend/app/assets/javascripts/tree_renderers.js.erb b/frontend/app/assets/javascripts/tree_renderers.js.erb new file mode 100644 index 0000000000..58591f462d --- /dev/null +++ b/frontend/app/assets/javascripts/tree_renderers.js.erb @@ -0,0 +1,245 @@ +function BaseRenderer() { + this.endpointMarkerTemplate = $('<tr class="end-marker" />'); + + this.rootTemplate = $('<tr> ' + + ' <td class="no-drag-handle"></td>' + + ' <td class="title"></td>' + + '</tr>'); + + + this.nodeTemplate = $('<tr> ' + + ' <td class="drag-handle"></td>' + + ' <td class="title"><span class="indentor"><button class="expandme"><i class="expandme-icon glyphicon glyphicon-chevron-right" /></button></span> </td>' + + '</tr>'); +} + +BaseRenderer.prototype.endpoint_marker = function () { + return this.endpointMarkerTemplate.clone(true); +} + +BaseRenderer.prototype.get_root_template = function () { + return this.rootTemplate.clone(false); +} + + +BaseRenderer.prototype.get_node_template = function () { + return this.nodeTemplate.clone(false); +}; + +BaseRenderer.prototype.i18n = function (enumeration, enumeration_value) { + if (EnumerationTranlations.hasOwnProperty(enumeration)) { + if (EnumerationTranlations[enumeration].hasOwnProperty(enumeration_value)) { + return EnumerationTranlations[enumeration][enumeration_value]; + } + } + return enumeration_value; +}; + + +function ResourceRenderer() { + BaseRenderer.call(this); + this.rootTemplate = $('<tr> ' + + ' <td class="no-drag-handle"></td>' + + ' <td class="title"></td>' + + ' <td class="resource-level"></td>' + + ' <td class="resource-type"></td>' + + ' <td class="resource-container"></td>' + + '</tr>'); + + this.nodeTemplate = $('<tr> ' + + ' <td class="drag-handle"></td>' + + ' <td class="title"><span class="indentor"><button class="expandme"><i class="expandme-icon glyphicon glyphicon-chevron-right" /></button></span> </td>' + + ' <td class="resource-level"></td>' + + ' <td class="resource-type"></td>' + + ' <td class="resource-container"></td>' + + '</tr>'); +} + +ResourceRenderer.prototype = Object.create(BaseRenderer.prototype); + +ResourceRenderer.prototype.add_root_columns = function (row, rootNode) { + var level = this.i18n('archival_record_level', rootNode.level); + var type = this.build_type_summary(rootNode); + var container_summary = this.build_container_summary(rootNode); + + row.find('.resource-level').text(level).attr('title', level); + row.find('.resource-type').text(type).attr('title', type); + row.find('.resource-container').text(container_summary).attr('title', container_summary); +} + + +ResourceRenderer.prototype.add_node_columns = function (row, node) { + var title = this.build_node_title(node); + var level = this.i18n('archival_record_level', node.level); + var type = this.build_type_summary(node); + var container_summary = this.build_container_summary(node); + + row.find('.title .record-title').text(title).attr('title', title); + row.find('.resource-level').text(level).attr('title', level); + row.find('.resource-type').text(type).attr('title', type); + row.find('.resource-container').text(container_summary).attr('title', container_summary); +}; + + +ResourceRenderer.prototype.build_node_title = function(node) { + var title_bits = []; + if (node.title) { + title_bits.push(node.title); + } + + if (node.dates && node.dates.length > 0) { + var first_date = node.dates[0]; + if (first_date.expression) { + title_bits.push(first_date.expression); + } else if (first_date.begin && first_date.end) { + title_bits.push(first_date.begin + '-' + first_date.end); + } else if (first_date.begin) { + title_bits.push(first_date.begin); + } + } + + return title_bits.join(', '); +}; + + +ResourceRenderer.prototype.build_type_summary = function(node) { + var self = this; + var type_summary = ''; + + if (node['containers']) { + var types = [] + + $.each(node['containers'], function(_, container) { + types.push(self.i18n('instance_instance_type', container['instance_type'])); + }); + + type_summary = types.join(', '); + } + + return type_summary; +}; + + +ResourceRenderer.prototype.build_container_summary = function(node) { + var self = this; + var container_summary = ''; + + if (node['containers']) { + var container_summaries = [] + + $.each(node['containers'], function(_, container) { + var summary_items = [] + if (container.top_container_indicator) { + var top_container_summary = ''; + + if (container.top_container_type) { + top_container_summary += self.i18n('container_type', container.top_container_type) + ': '; + } + + top_container_summary += container.top_container_indicator; + + if (container.top_container_barcode) { + top_container_summary += ' [' + container.top_container_barcode + ']'; + } + + summary_items.push(top_container_summary); + } + if (container.type_2) { + summary_items.push(self.i18n('container_type', container.type_2) + ': ' + container.indicator_2); + } + if (container.type_3) { + summary_items.push(self.i18n('container_type', container.type_3) + ': ' + container.indicator_3); + } + container_summaries.push(summary_items.join(', ')); + }); + + container_summary = container_summaries.join('; '); + } + + return container_summary; +}; + + +function DigitalObjectRenderer() { + BaseRenderer.call(this); + + + this.rootTemplate = $('<tr> ' + + ' <td class="no-drag-handle"></td>' + + ' <td class="title"></td>' + + ' <td class="digital-object-type"></td>' + + ' <td class="file-uri-summary"></td>' + + '</tr>'); + + + this.nodeTemplate = $('<tr> ' + + ' <td class="drag-handle"></td>' + + ' <td class="title"><span class="indentor"><button class="expandme"><i class="expandme-icon glyphicon glyphicon-chevron-right" /></button></span> </td>' + + ' <td class="digital-object-type"></td>' + + ' <td class="file-uri-summary"></td>' + + '</tr>'); +} + +DigitalObjectRenderer.prototype = BaseRenderer.prototype; + +DigitalObjectRenderer.prototype.add_root_columns = function (row, rootNode) { + if (rootNode.digital_object_type) { + var type = this.i18n('digital_object_digital_object_type', rootNode.digital_object_type); + row.find('.digital-object-type').text(type).attr('title', type); + } + + if (rootNode.file_uri_summary) { + row.find('.file-uri-summary').text(rootNode.file_uri_summary).attr('title', rootNode.file_uri_summary); + } +} + +DigitalObjectRenderer.prototype.add_node_columns = function (row, node) { + var title = this.build_node_title(node); + + row.find('.title .record-title').text(title).attr('title', title); + row.find('.file-uri-summary').text(node.file_uri_summary).attr('title', node.file_uri_summary); +}; + +DigitalObjectRenderer.prototype.build_node_title = function(node) { + var title_bits = []; + + if (node.title) { + title_bits.push(node.title); + } + + if (node.label) { + title_bits.push(node.label); + } + + if (node.dates && node.dates.length > 0) { + var first_date = node.dates[0]; + if (first_date.expression) { + title_bits.push(first_date.expression); + } else if (first_date.begin && first_date.end) { + title_bits.push(first_date.begin + '-' + first_date.end); + } else if (first_date.begin) { + title_bits.push(first_date.begin); + } + } + + return title_bits.join(', '); +}; + +function ClassificationRenderer() { + BaseRenderer.call(this); +} + +ClassificationRenderer.prototype = BaseRenderer.prototype; + + +var EnumerationTranlations = {}; +<% ['instance_instance_type', 'container_type', 'resource_resource_type', + 'archival_record_level', 'digital_object_digital_object_type' + ].each do |enumeration_name| + translations = {} + JSONModel.enum_values(enumeration_name).each do |enumeration_value| + translations[enumeration_value] = I18n.t("enumerations.#{enumeration_name}.#{enumeration_value}", :default => enumeration_value) + end +%> + EnumerationTranlations['<%= enumeration_name %>'] = <%= ASUtils.to_json(translations) %>; +<% end %> \ No newline at end of file diff --git a/frontend/app/assets/javascripts/tree_resizer.js b/frontend/app/assets/javascripts/tree_resizer.js new file mode 100644 index 0000000000..044efb5490 --- /dev/null +++ b/frontend/app/assets/javascripts/tree_resizer.js @@ -0,0 +1,84 @@ +//= require jquery.cookie + +var DEFAULT_TREE_PANE_HEIGHT = 100; +var DEFAULT_TREE_MIN_HEIGHT = 60; + +function TreeResizer(tree, container) { + this.tree = tree; + this.container = container; + + this.setup(); +}; + +TreeResizer.prototype.setup = function() { + var self = this; + + self.resize_handle = $('<div class="ui-resizable-handle ui-resizable-s" />'); + self.container.after(self.resize_handle); + + self.container.resizable({ + handles: { + s: self.resize_handle, + }, + minHeight: DEFAULT_TREE_MIN_HEIGHT, + resize: function(event, ui) { + self.resize_handle.removeClass("maximized"); + self.set_height(ui.size.height); + } + }); + + self.$toggle = $('<a>').addClass('tree-resize-toggle'); + self.resize_handle.append(self.$toggle); + + self.$toggle.on('click', function() { + self.toggle_height(); + }); + + self.reset(); +} + +TreeResizer.prototype.get_height = function() { + if (AS.prefixed_cookie("archives-tree-container::height")) { + return AS.prefixed_cookie("archives-tree-container::height"); + } else { + return DEFAULT_TREE_PANE_HEIGHT; + } +}; + +TreeResizer.prototype.set_height = function(height) { + AS.prefixed_cookie("archives-tree-container::height", height); +}; + +TreeResizer.prototype.maximize = function(margin) { + if (margin === undefined) { + margin = 50; + } + + this.resize_handle.addClass("maximized"); + this.container.height($(window).height() - margin - this.container.offset().top); +}; + +TreeResizer.prototype.reset = function() { + this.container.height(this.get_height()); +}; + +TreeResizer.prototype.minimize = function() { + this.resize_handle.removeClass("maximized"); + this.container.height(DEFAULT_TREE_MIN_HEIGHT); + document.body.scrollTop = this.tree.toolbar_renderer.container.offset().top - 5; +}; + +TreeResizer.prototype.maximized = function() { + return this.resize_handle.is('.maximized'); +} + +TreeResizer.prototype.toggle_height = function() { + var self = this; + + if (self.maximized()) { + self.minimize(); + } else { + self.maximize(); + } +}; + diff --git a/frontend/app/assets/javascripts/tree_toolbar.js.erb b/frontend/app/assets/javascripts/tree_toolbar.js.erb new file mode 100644 index 0000000000..14c64a57e2 --- /dev/null +++ b/frontend/app/assets/javascripts/tree_toolbar.js.erb @@ -0,0 +1,622 @@ +var SHARED_TOOLBAR_ACTIONS = [ + { + label: '<%= I18n.t('actions.enable_reorder') %>', + cssClasses: 'btn-default drag-toggle', + onRender: function(btn, node, tree, toolbarRenderer) { + if ($(tree.large_tree.elt).is('.drag-enabled')) { + $(btn).addClass('active').addClass('btn-success'); + + $(btn).text('<%= I18n.t('actions.reorder_active') %>'); + + tree.ajax_tree.hide_form(); + } + }, + onToolbarRendered: function(btn, toolbarRenderer) { + if ($(tree.large_tree.elt).is('.drag-enabled')) { + $('.btn:not(.drag-toggle,.finish-editing,.cut-selection,.paste-selection,.move-node)',toolbarRenderer.container).hide(); + $('.cut-selection',toolbarRenderer.container).removeClass('disabled'); + if ($('.cut', tree.large_tree.elt).length > 0) { + $('.paste-selection',toolbarRenderer.container).removeClass('disabled'); + } + } + }, + onClick: function(event, btn, node, tree, toolbarRenderer) { + $(tree.large_tree.elt).toggleClass('drag-enabled'); + $(event.target).toggleClass('btn-success'); + + if ($(tree.large_tree.elt).is('.drag-enabled')) { + $(btn).text('<%= I18n.t('actions.reorder_active') %>'); + tree.ajax_tree.hide_form(); + $.scrollTo(0); + tree.resizer.maximize(DRAGDROP_HOTSPOT_HEIGHT); + $('.btn:not(.drag-toggle,.finish-editing)',toolbarRenderer.container).hide(); + $('.cut-selection,.paste-selection,.move-node', toolbarRenderer.container).show(); + $('.cut-selection,.move-node',toolbarRenderer.container).removeClass('disabled'); + } else { + $(btn).text('<%= I18n.t('actions.enable_reorder') %>'); + tree.ajax_tree.show_form(); + tree.resizer.reset(); + $('.btn',toolbarRenderer.container).show(); + $('.cut-selection,.paste-selection,.move-node', toolbarRenderer.container).hide(); + $(btn).blur(); + } + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormChanged: function(btn, form, tree, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + if ($(tree.large_tree.elt).is('.drag-enabled')) { + tree.ajax_tree.blockout_form(); + } + }, + }, + [ + { + label: '<%= I18n.t("actions.cut") %>', + cssClasses: 'btn-default cut-selection', + onRender: function(btn, node, tree, toolbarRenderer) { + if (!$(tree.large_tree.elt).is('.drag-enabled')) { + btn.hide(); + } + }, + onClick: function(event, btn, node, tree, toolbarRenderer) { + event.preventDefault(); + // clear the previous cut set + $('.cut', tree.large_tree.elt).removeClass('cut'); + + var rowsToCut = []; + if (tree.dragdrop.rowsToMove.length > 0) { + // if multiselected rows, cut them + rowsToCut = $.merge([], tree.dragdrop.rowsToMove); + } else if (tree.current().is(':not(.root-row)')) { + // otherwise cut the current row + rowsToCut = [tree.current()]; + } + + if (rowsToCut.length > 0) { + $.each(rowsToCut, function(_, row) { + $(row).addClass('cut'); + }); + + $('.paste-selection', toolbarRenderer.container).removeClass('disabled'); + } + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only && tree.dragdrop; + } + }, + { + label: '<%= I18n.t("actions.paste") %>', + cssClasses: 'btn-default paste-selection', + onRender: function(btn, node, tree, toolbarRenderer) { + if (!$(tree.large_tree.elt).is('.drag-enabled')) { + btn.hide(); + } else if ($('.cut', $(tree.large_tree.elt)).length == 0) { + btn.addClass('disabled'); + } + }, + onClick: function(event, btn, node, tree, toolbarRenderer) { + event.preventDefault(); + var current = tree.current(); + var cut = $('.cut', tree.large_tree.elt); + + var rowsToPaste = []; + cut.each(function(_,row) { + if ($(row).data('uri') != current.data('uri')) { + rowsToPaste.push(row); + } + }); + + tree.large_tree.reparentNodes(current, rowsToPaste, current.data('child_count')).done(function() { + $('.cut', tree.large_tree.elt).removeClass('cut'); + }); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only && tree.dragdrop; + } + }, + ], + { + label: '<%= I18n.t "actions.move" %> <span class="caret"></span>', + cssClasses: 'btn-default dropdown-toggle move-node', + groupCssClasses: 'dropdown', + onRender: function(btn, node, tree, toolbarRenderer) { + if (!$(tree.large_tree.elt).is('.drag-enabled')) { + btn.hide(); + } + var level = node.data('level'); + var position = node.data('position'); + + var $options = $('<ul>').addClass('dropdown-menu '); + // move up a level + if (level > 1) { + var $li = $('<li>'); + $li.append($('<a>').attr('href', 'javascript:void(0);'). + addClass('move-node move-node-up-level'). + text('<%= I18n.t('actions.move_up_a_level') %>')); + $options.append($li); + } + + var $prevAtLevel = node.prevAll('.largetree-node.indent-level-'+level+':first'); + var $nextAtLevel = node.nextAll('.largetree-node.indent-level-'+level+':first'); + + // move up on same level + if ($prevAtLevel.length > 0) { + var $li = $('<li>'); + $li.append($('<a>').attr('href', 'javascript:void(0);') + .addClass('move-node move-node-up') + .text('<%= I18n.t('actions.move_up') %>')); + $options.append($li); + } + // move down on same level + if ($nextAtLevel.length > 0) { + var $li = $('<li>'); + $li.append($('<a>').attr('href', 'javascript:void(0);') + .addClass('move-node move-node-down') + .text('<%= I18n.t('actions.move_down') %>')); + $options.append($li); + } + // move down into sibling + if ($prevAtLevel.length > 0 || $nextAtLevel.length > 0) { + var $li = $('<li>').addClass('dropdown-submenu'); + $li.append($('<a>').attr('href', 'javascript:void(0);') + .text('<%= I18n.t('actions.move_down_into') %>')); + $options.append($li); + + // add nearest siblings to dropdown list + // 1 up and 1 below + var $siblings = $('<ul>').addClass('dropdown-menu').addClass('move-node-into-menu'); + if ($prevAtLevel.length > 0) { + var $subli = $('<li>'); + $subli.append($('<a>').attr('href', 'javascript:void(0);') + .addClass('move-node move-node-down-into') + .attr('data-uri', $prevAtLevel.data('uri')) + .attr('data-tree_id', $prevAtLevel.attr('id')) + .text($prevAtLevel.find('.record-title').text().trim())); + $siblings.append($subli); + } + if ($nextAtLevel.length > 0) { + var $subli = $('<li>'); + $subli.append($('<a>').attr('href', 'javascript:void(0);') + .addClass('move-node move-node-down-into') + .attr('data-uri', $nextAtLevel.data('uri')) + .attr('data-tree_id', $nextAtLevel.attr('id')) + .text($nextAtLevel.find('.record-title').text().trim())); + $siblings.append($subli); + } + $siblings.appendTo($li); + } + $options.appendTo(btn.closest('.btn-group')); + + $options.on('click', '.move-node-up-level', function(event) { + // move node to last child of parent + var $new_parent = node.prevAll('.indent-level-'+(level-2)+":first"); + tree.large_tree.reparentNodes($new_parent, node, $new_parent.data('child_count')); + }).on('click', '.move-node-up', function(event) { + // move node above nearest sibling + var $parent = node.prevAll('.indent-level-'+(level-1)+":first"); + var $prev = node.prevAll('.indent-level-'+(level)+":first"); + tree.large_tree.reparentNodes($parent, node, $prev.data('position')); + }).on('click', '.move-node-down', function(event) { + // move node below nearest sibling + var $parent = node.prevAll('.indent-level-'+(level-1)+":first"); + var $next = node.nextAll('.indent-level-'+(level)+":first"); + tree.large_tree.reparentNodes($parent, node, $next.data('position')+1); + }).on('click', '.move-node-down-into', function(event) { + // move node to last child of sibling + var $parent = $('#'+$(this).data('tree_id')); + tree.large_tree.reparentNodes($parent, node, $parent.data('child_count')); + }); + + btn.attr('data-toggle', 'dropdown'); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + // not available to root nodes + if (node.is('.root-row')) { + return false; + } + + return !tree.large_tree.read_only && tree.dragdrop; + }, + }, + // RDE + { + label: '<%= I18n.t("actions.rapid_data_entry") %>', + cssClasses: 'btn-default add-children', + onClick: function(event, btn, node, tree, toolbarRenderer) { + $(document).triggerHandler("rdeshow.aspace", [node, btn]); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + }, + // go back to the read only page + { + label: '<%= I18n.t("actions.finish_editing") %>', + cssClasses: 'btn-success finish-editing', + groupCssClasses: 'pull-right', + onRender: function(btn, node, tree, toolbarRenderer) { + var readonlyPath = location.pathname.replace(/\/edit$/, ''); + btn.attr('href', readonlyPath + location.hash); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + } + }, +]; + +var TreeToolbarConfiguration = { + resource: [].concat(SHARED_TOOLBAR_ACTIONS).concat([ + { + label: '<%= I18n.t("resource._frontend.action.add_child") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level') + 1); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + } + ]), + + archival_object: [].concat(SHARED_TOOLBAR_ACTIONS).concat([ + [ + { + label: '<%= I18n.t("archival_object._frontend.action.add_child") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level') + 1); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + }, + { + label: '<%= I18n.t("archival_object._frontend.action.add_sibling") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level')); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + } + ], + { + label: '<%= I18n.t("actions.transfer") %>', + cssClasses: 'btn-default dropdown-toggle transfer-node', + groupCssClasses: 'dropdown', + onRender: function(btn, node, tree, toolbarRenderer) { + var $li = btn.parent(); + btn.replaceWith(AS.renderTemplate('tree_toolbar_transfer_action', { + node_id: TreeIds.uri_to_parts(node.data('uri')).id, + root_object_id: TreeIds.uri_to_parts(tree.large_tree.root_uri).id, + })); + $(".linker:not(.initialised)", $li).linker() + + var $form = $li.find('form'); + $form.on('submit', function(event) { + if ($(this).serializeObject()['transfer[ref]']) { + // continue with the POST + return; + } else { + event.stopPropagation(); + event.preventDefault(); + $(".missing-ref-message", $form).show(); + return true; + } + }).on('click', '.btn-cancel', function(event) { + event.preventDefault(); + event.stopPropagation(); + $(this).closest('.btn-group.dropdown').toggleClass("open"); + }).on('click', ':input', function(event) { + event.stopPropagation(); + }).on("click", ".dropdown-toggle", function(event) { + event.stopPropagation(); + $(this).parent().toggleClass("open"); + }); + $li.on('shown.bs.dropdown', function() { + $("#token-input-transfer_ref_", $form).focus(); + }); + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + } + ]), + + digital_object: [].concat(SHARED_TOOLBAR_ACTIONS).concat([ + { + label: '<%= I18n.t("digital_object._frontend.action.add_child") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level') + 1); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + } + ]), + + digital_object_component: [].concat(SHARED_TOOLBAR_ACTIONS).concat([ + [ + { + label: '<%= I18n.t("digital_object_component._frontend.action.add_child") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level') + 1); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + }, + { + label: '<%= I18n.t("digital_object_component._frontend.action.add_sibling") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level')); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + } + ] + ]), + + classification: [].concat(SHARED_TOOLBAR_ACTIONS).concat([ + { + label: '<%= I18n.t("classification._frontend.action.add_child") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level') + 1); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + } + ]), + + classification_term: [].concat(SHARED_TOOLBAR_ACTIONS).concat([ + [ + { + label: '<%= I18n.t("classification_term._frontend.action.add_child") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level') + 1); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + }, + { + label: '<%= I18n.t("classification_term._frontend.action.add_sibling") %>', + cssClasses: 'btn-default', + onClick: function(event, btn, node, tree, toolbarRenderer) { + tree.ajax_tree.add_new_after(node, node.data('level')); + }, + isEnabled: function(node, tree, toolbarRenderer) { + return true; + }, + isVisible: function(node, tree, toolbarRenderer) { + return !tree.large_tree.read_only; + }, + onFormLoaded: function(btn, form, tree, toolbarRenderer) { + $(btn).removeClass('disabled'); + }, + onToolbarRendered: function(btn, toolbarRenderer) { + $(btn).addClass('disabled'); + }, + }, + ] + ]), +}; + +function TreeToolbarRenderer(tree, container) { + this.tree = tree; + this.container = container; +} + +TreeToolbarRenderer.prototype.reset = function() { + if (this.current_node) { + this.render(this.current_node); + } +}; + +TreeToolbarRenderer.prototype.reset_callbacks = function() { + this.on_form_changed_callbacks = []; + this.on_form_loaded_callbacks = []; + this.on_toolbar_rendered_callbacks = []; +}; + +TreeToolbarRenderer.prototype.render = function(node) { + var self = this; + + if (self.current_node) { + self.reset_callbacks(); + } + + self.current_node = node; + + var actions = TreeToolbarConfiguration[node.data('jsonmodel_type')]; + self.container.empty(); + + if (actions == null) { + return + } + + self.reset_callbacks(); + + actions.map(function(action_or_group) { + if (!$.isArray(action_or_group)) { + action_group = [action_or_group]; + } else { + action_group = action_or_group; + } + var $group = $('<div>').addClass('btn-group'); + self.container.append($group); + + action_group.map(function(action) { + if (action.isVisible == undefined || $.proxy(action.isVisible, tree)(node, tree, self)) { + var $btn = $('<a>').addClass('btn btn-xs'); + $btn.html(action.label).addClass(action.cssClasses).attr('href', 'javascript:void(0)'); + + if (action.isEnabled == undefined || $.proxy(action.isEnabled, tree)(node, tree, self)) { + if (action.onClick) { + $btn.on('click', function (event) { + return $.proxy(action.onClick, tree)(event, $btn, node, tree, self); + }); + } + } else { + $btn.addClass('disabled'); + } + + if (action.groupCssClasses) { + $group.addClass(action.groupCssClasses); + } + + if (action.onFormChanged) { + self.on_form_changed_callbacks.push(function(form) { + $.proxy(action.onFormChanged, tree)($btn, form, tree, self); + }); + } + + if (action.onFormLoaded) { + self.on_form_loaded_callbacks.push(function(form) { + $.proxy(action.onFormLoaded, tree)($btn, form, tree, self); + }); + } + + if (action.onToolbarRendered) { + self.on_toolbar_rendered_callbacks.push(function() { + $.proxy(action.onToolbarRendered, tree)($btn, self); + }); + } + + $group.append($btn); + + if (action.onRender) { + $.proxy(action.onRender, tree)($btn, node, tree, self); + } + } + }); + + if ($group.length == 0) { + $group.remove(); + } + }); + + $.each(self.on_toolbar_rendered_callbacks, function(i, callback) { + callback(); + }); +}; + +TreeToolbarRenderer.prototype.notify_form_changed = function(form) { + $.each(this.on_form_changed_callbacks, function(i, callback) { + callback(form); + }); +}; + +TreeToolbarRenderer.prototype.notify_form_loaded = function(form) { + $.each(this.on_form_loaded_callbacks, function(i, callback) { + callback(form); + }); +}; diff --git a/frontend/app/assets/javascripts/utils.js b/frontend/app/assets/javascripts/utils.js index 39ba3d378d..50c0aa4a8e 100644 --- a/frontend/app/assets/javascripts/utils.js +++ b/frontend/app/assets/javascripts/utils.js @@ -139,13 +139,23 @@ $(function() { scope = scope || $(document.body); $(".date-field:not(.initialised)", scope).each(function() { var $dateInput = $(this); - $dateInput.wrap("<div class='input-group date'></div>"); + + if ($dateInput.parent().is(".input-group")) { + $dateInput.parent().addClass("date"); + } else { + $dateInput.wrap("<div class='input-group date'></div>"); + } + $dateInput.addClass("initialised"); - var $addon = "<span class='input-group-addon'><i class='glyphicon glyphicon-calendar'></i></span>" + var $addon = $("<span class='input-group-addon'><i class='glyphicon glyphicon-calendar'></i></span>"); $dateInput.after($addon); - $dateInput.parent(".date").datepicker($dateInput.data()); + $dateInput.datepicker($dateInput.data()); + + $addon.on("click", function() { + $dateInput.focus().select(); + }); }); }; initDateFields(); @@ -195,9 +205,7 @@ $(function() { scope = scope || $(document.body); $(".has-popover:not(.initialised)", scope) .popover(popoverOptions) - .click(function(e) { - e.preventDefault(); - }).addClass("initialised"); + .addClass("initialised"); }; initPopovers(); $(document).bind("loadedrecordform.aspace init.popovers", function(event, $container) { diff --git a/frontend/app/assets/stylesheets/archivesspace.less b/frontend/app/assets/stylesheets/archivesspace.less index 386143a04a..5e687d3aac 100644 --- a/frontend/app/assets/stylesheets/archivesspace.less +++ b/frontend/app/assets/stylesheets/archivesspace.less @@ -30,6 +30,5 @@ @import "../../codemirror/codemirror.css"; @import "../../codemirror/util/simple-hint.css"; @import "../../css-spinners/spinner.css"; -@import "../../jstree/style.css"; @import "../../bootstrap-select/bootstrap-select.css"; @import "../../jquery.tablesorter/theme.bootstrap.min.css"; diff --git a/frontend/app/assets/stylesheets/archivesspace/form.less b/frontend/app/assets/stylesheets/archivesspace/form.less index 4fd1ee67da..848e2e4433 100644 --- a/frontend/app/assets/stylesheets/archivesspace/form.less +++ b/frontend/app/assets/stylesheets/archivesspace/form.less @@ -2,6 +2,13 @@ // Increase spacing between groups .form-group { margin-bottom: 8px; + + // fix styling of inline-help when next to a checkbox + .checkbox { + .help-inline { + margin-left: 20px; + } + } } } diff --git a/frontend/app/assets/stylesheets/archivesspace/largetree.less b/frontend/app/assets/stylesheets/archivesspace/largetree.less new file mode 100644 index 0000000000..7d839625fd --- /dev/null +++ b/frontend/app/assets/stylesheets/archivesspace/largetree.less @@ -0,0 +1,325 @@ +// Note! This file is used both in the frontend (LESS) and pui +// (SASS). Keep the rules here as close to vanilla CSS as +// possible--or, at least, a compatible subset of LESS/SCSS! + +.largetree-container { + background-color: #EEE; + margin-bottom: 1em; + overflow-x: auto; + overflow-y: auto; + padding: 1px; // to allow for outline on current row + + .reparented-highlight .title { + background-color: #93d093; + transition: all 0.5s linear; + } + + .reparented .title { + background-color: auto; + transition: all 1.5s linear; + } + + button.expandme { + background: none; + border: 0; + } + + .waypoint { + height: 0; + display: table-row; + } + + li { + padding: 0; + margin: 0.5em 0; + } + + .waypoint.populated { + display: none; + } + + .root { + width: 100%; + } + + table { + table-layout: fixed; + } + + tr.multiselected-row { + outline: 1px solid #0A6AA1; + + td { + background-color: #b6def5; + } + + } + + td.drag-handle, + td.no-drag-handle { + display: none; + } + &.drag-enabled { + td.drag-handle, + td.no-drag-handle { + display: table-cell; + } + td.no-drag-handle { + width: 1em; + } + td.drag-handle { + width: 1em; + background-image: asset-url('archivesspace/tree_drag_handle.gif'); + background-repeat: no-repeat; + background-position: 50% 50%; + cursor: move; + } + td.drag-handle.drag-disabled { visibility: hidden; } + td.drag-handle.multiselected { + width: 1em; + background-color: #0A6AA1; + } + } + + td.resource-level { width: 8%; } + td.resource-type { width: 18%; } + td.resource-container { width: 28%; } + + td.digital-object-type { width: 12%; } + td.file-uri-summary { width: 28%; } + + + td { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + + // Don't change this without also changing largetree.js.erb -> + // appendWaypoints to match. + // + // We rely on rows having heights known in advance to do our + // height calculations. + height: 2em; + line-height: 2em; + } + + .expandme:focus { + border: 0; + outline-style: none; + } + + .expandme-icon.expanded { + transform: rotate(90deg); + transition:transform 100ms ease-out; + } + + tr.current { + outline: 1px solid #0A6AA1; + + td { + background-color: #EFFAFF; + } + } + + .indentor { + display: inline-block; + height: inherit; + text-align: right; + background-color: #F1F1F1; + background-image: repeating-linear-gradient(90deg, transparent, transparent 23px, #FFFFFF 24px); + } + + table.root { + tbody { + tr:nth-of-type(odd) { + background-color: #FAFAFA; + } + tr:first-child { + background-color: #F1F1F1; + td.title { + padding-left: 6px + } + } + } + } + + .indent-level-0 .indentor { width: 24px } + .indent-level-1 .indentor { width: 24px } + .indent-level-2 .indentor { width: 48px } + .indent-level-3 .indentor { width: 72px } + .indent-level-4 .indentor { width: 96px } + .indent-level-5 .indentor { width: 120px } + .indent-level-6 .indentor { width: 144px } + .indent-level-7 .indentor { width: 168px } + .indent-level-8 .indentor { width: 192px } + .indent-level-9 .indentor { width: 216px } + .indent-level-10 .indentor { width: 240px; } + .indent-level-11 .indentor { width: 264px; } + .indent-level-12 .indentor { width: 288px; } + .indent-level-13 .indentor { width: 312px; } + .indent-level-14 .indentor { width: 336px; } + .indent-level-15 .indentor { width: 360px; } + .indent-level-16 .indentor { width: 384px; } + + tr#new { + .indentor, .new-title { + float: left; + } + .new-title { + margin-left: 4px; + line-height: 2em; + } + .indentor { + .glyphicon { + line-height: 2em; + margin-right: 4px; + color: #4cae4c; + } + } + } + + tr.cut { + td { + background-color: #CCC; + box-shadow: inset 0 1px 2px #999; + } + } +} + +.largetree-progress-indicator { + width: 100%; + height: 5px; + display: block; + visibility: hidden; +} + +.ui-resizable-handle.ui-resizable-s { + width: 100%; + position: absolute; + bottom: 0; + left: 0; + height: 8px; + background-color: #EEE; + background-image: asset-url("archivesspace/drag_handle.png"); + background-repeat: no-repeat; + background-position: center center; + border-top: 1px solid #DDD; + border-bottom: 1px solid #D9D9D9; + cursor: ns-resize; + + &:hover { + background-color: #e9e9e9; + } + + .tree-resize-toggle { + font-family: 'Glyphicons Halflings'; + position: absolute; + font-size: 8px; + right: 0; + top: 0px; + padding: 0 20px; + cursor: pointer; + background: rgba(0,0,0,0.1); + line-height: 8px; + color: #888; + + &:before { + content: '\e114'; + } + + &:hover { + text-decoration: none; + } + } + + &.maximized { + .tree-resize-toggle { + &:before { + content: '\e113'; + } + } + } +} + +.ui-resizable-resizing { + .ui-resizable-handle.ui-resizable-s { + background-color: #e9e9e9; + } +} + +.tree-drag-indicator { + position: fixed; + display: inline-block; + z-index: 1000; + background-color: white; + padding: 5px; + border: solid black 1px; + + ul { + margin: 0; + padding: 0; + padding-left: 1.5em; + } +} + +.largetree-container.drag-enabled { + tr.drag-drop-over { + outline: 1px solid rgb(2, 132, 130); + td { + background-color: #F0FFF0; + } + } + tr.drag-drop-over-disallowed { + outline: 1px solid #c82829; + td { + background-color: #FFF0F0; + } + } +} + +.tree-scroll-hotspot { + position: absolute; + opacity: 0; + z-index: 900; +} + +#object_container { + .blockout { + position: absolute; + top: 0; + left: 0; + background: rgba(0,0,0,0.2); + z-index: 100; + } +} +.largetree-blockout { + position: fixed; + height: 100%; + width: 100%; + background-color: rgba(0,0,0,0.3); + z-index: 200; + top: 0; + left: 0; +} + +#tree-toolbar { + padding: 4px 5px 4px 0; + background-image: linear-gradient(to bottom, #f3f3f3 0%, #ededed 100%); + margin: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid #DFDFDF; + + .tree-transfer-form { + form { + margin: 0; + } + fieldset { + width: 300px; + padding: 10px; + } + .form-actions { + margin: 0; + } + } +} diff --git a/frontend/app/assets/stylesheets/archivesspace/tree.less b/frontend/app/assets/stylesheets/archivesspace/tree.less index 8e1571626a..34da0dcb48 100644 --- a/frontend/app/assets/stylesheets/archivesspace/tree.less +++ b/frontend/app/assets/stylesheets/archivesspace/tree.less @@ -2,15 +2,6 @@ position: relative; } -.archives-tree.jstree-default.jstree-focused { - background-color: transparent; -} - -.archives-tree .resource .jstree-themeicon { - background-image: none; -} - - .archives-tree-container { margin: -14px -15px 10px -15px !important; padding: 10px 0 10px 0; @@ -42,129 +33,6 @@ .retract-tree { display: none; } - - .archives-tree { - padding: 0 20px 0 0; - margin: 0 0 0 20px; - overflow: auto; - - &.overflow { - .box-shadow(inset -1px 2px 3px #DDD); - } - - .jstree-icon { - float: left; - } - - li { - clear: both; - overflow: hidden; - - .jstree-hovered { - > .tree-node-text { - .field-column { - background-color: transparent !important; - } - } - } - &.selected { - > a { - background-color: #d9f4ff; - > .tree-node-text { - .field-column { - background-color: transparent !important; - } - } - } - } - &.primary-selected { - > a { - background-color: #beebff; - } - } - &.cut-to-clipboard { - > ins, > a { - .opacity(50); - } - } - } - - a { - display: block; - padding: 2px 0px !important; - border: 1px solid transparent; - line-height: 16px; - height: auto !important; - overflow: hidden; - - .title-column { - margin-left: 0; - white-space: normal; - width: 50%; - } - - .field-column { - width: 120px; - margin-left: 2px; - color: #666; - padding-left: 4px; - overflow: hidden; - - &.field-column-1 { - width: 90px; - background-color: #e8e8e8; - } - &.field-column-2 { - width: 150px; - background-color: #e8e8e8; - } - &.field-column-3 { - width: 200px; - background-color: #e8e8e8; - } - } - - &.jstree-clicked { - background-color: transparent; - } - - &.jstree-hovered { - background-color: #e7f4f9; - border: 1px dotted #CCC; - } - } - - .digital_object { - a { - .field-column-1 { - width: 100px; - } - .field-column-2 { - width: 350px; - } - .field-column-3 { - display: none; - } - } - } - - .classification { - a { - .title-column { - width: 90% !important; - } - } - } - - a > .jstree-icon { - display: inline-block; - .ie7-restore-right-whitespace(); - height: 100%; - line-height: 14px; - vertical-align: text-top; - /* padding-right: 20px; */ - } - } .spinner { display: none; @@ -200,33 +68,6 @@ color: #ddd !important; } } - .ui-resizable-handle { - z-index: auto !important; - } - .ui-resizable-handle.ui-resizable-s { - width: 100%; - position: absolute; - bottom: 0; - left: 0; - height: 8px; - background-color: #EEE; - background-image: asset-url("archivesspace/drag_handle.png"); - background-repeat: no-repeat; - background-position: center center; - border-top: 1px solid #DDD; - border-bottom: 1px solid #D9D9D9; - cursor: ns-resize; - - &:hover { - background-color: #e9e9e9; - } - } - - &.ui-resizable-resizing { - .ui-resizable-handle.ui-resizable-s { - background-color: #e9e9e9; - } - } .tree-transfer-form { form { @@ -243,15 +84,6 @@ } -a > .jstree-icon { - background-position: -1000px -1000px !important; -} - -.jstree-open > a > .jstree-icon { - background-position: -408px -120px !important; -} - - .archives-tree-container.expanded { .box-shadow(1px 1px 5px rgba(0, 0, 0, 0.4)); .opacity(95); @@ -285,19 +117,6 @@ a > .jstree-icon { z-index: 1; } -.glyphicon-refresh-animate { - -animation: spin 1s infinite linear; - -webkit-animation: spin2 1s infinite linear; -} - -.jstree-icon.spinner { - padding-right: 0 !important; - font-size: 5px; - margin-right: 10px !important; -} - - - @-webkit-keyframes spin2 { from { -webkit-transform: rotate(0deg);} to { -webkit-transform: rotate(360deg);} @@ -308,68 +127,6 @@ a > .jstree-icon { to { transform: scale(1) rotate(360deg);} } -.jstree-default a.jstree-loading .jstree-icon { - background:none !important; -} - -@media (max-width: 1200px) { - .archives-tree-container { - .archives-tree { - a { - .field-column { - &.field-column-2 { - width: 110px; - } - &.field-column-3 { - width: 180px; - } - } - } - .digital_object { - a { - .field-column-1 { - width: 100px; - } - .field-column-2 { - width: 200px; - } - .field-column-3 { - display: none; - } - } - } - } - } -} - -@media (max-width: 980px) { - .archives-tree-container { - .archives-tree { - a { - .field-column-3 { - display: none; - } - } - } - } -} - -@media (max-width: 767px) { - .archives-tree-container { - .archives-tree { - a { - .title-column { - width: auto; - float: none; - } - .field-column { - display: none; - } - } - } - } -} - @media (max-width: 600px) { #archives_tree_toolbar { .btn-success { diff --git a/frontend/app/controllers/agents_controller.rb b/frontend/app/controllers/agents_controller.rb index a9b781794e..2fa5b2cfad 100644 --- a/frontend/app/controllers/agents_controller.rb +++ b/frontend/app/controllers/agents_controller.rb @@ -6,7 +6,7 @@ class AgentsController < ApplicationController "manage_repository" => [:defaults, :update_defaults] - before_filter :assign_types + before_action :assign_types include ExportHelper diff --git a/frontend/app/controllers/application_controller.rb b/frontend/app/controllers/application_controller.rb index f1d1a54c16..053764a7a6 100644 --- a/frontend/app/controllers/application_controller.rb +++ b/frontend/app/controllers/application_controller.rb @@ -14,23 +14,23 @@ class ApplicationController < ActionController::Base # Allow overriding of templates via the local folder(s) if not ASUtils.find_local_directories.blank? - ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'frontend', 'views')}.reject { |dir| !Dir.exists?(dir) }.each do |template_override_directory| + ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'frontend', 'views')}.reject { |dir| !Dir.exist?(dir) }.each do |template_override_directory| prepend_view_path(template_override_directory) end end # Note: This should be first! - before_filter :store_user_session + before_action :store_user_session - before_filter :determine_browser_support + before_action :determine_browser_support - before_filter :refresh_permissions + before_action :refresh_permissions - before_filter :refresh_preferences + before_action :refresh_preferences - before_filter :load_repository_list + before_action :load_repository_list - before_filter :unauthorised_access + before_action :unauthorised_access def self.permission_mappings Array(@permission_mappings) @@ -50,12 +50,12 @@ def self.can_access?(context, method) def self.set_access_control(permission_mappings) @permission_mappings = permission_mappings - skip_before_filter :unauthorised_access, :only => Array(permission_mappings.values).flatten.uniq + skip_before_action :unauthorised_access, :only => Array(permission_mappings.values).flatten.uniq permission_mappings.each do |permission, actions| next if permission === :public - before_filter(:only => Array(actions)) {|c| user_must_have(permission)} + before_action(:only => Array(actions)) {|c| user_must_have(permission)} end end @@ -397,6 +397,11 @@ def determine_browser_support protected def cleanup_params_for_schema(params_hash, schema) + # We're expecting a HashWithIndifferentAccess... + if params_hash.respond_to?(:to_unsafe_hash) + params_hash = params_hash.to_unsafe_hash + end + fix_arrays = proc do |hash, schema| result = hash.clone @@ -498,7 +503,7 @@ def cleanup_params_for_schema(params_hash, schema) end def params_for_backend_search - params_for_search = params.select{|k,v| ["page", "q", "type", "sort", "exclude", "filter_term", "simple_filter"].include?(k) and not v.blank?} + params_for_search = params.select{|k,v| ["page", "q", "aq", "type", "sort", "exclude", "filter_term"].include?(k) and not v.blank?} params_for_search["page"] ||= 1 @@ -511,10 +516,10 @@ def params_for_backend_search params_for_search["filter_term[]"] = Array(params_for_search["filter_term"]).reject{|v| v.blank?} params_for_search.delete("filter_term") end - - if params_for_search["simple_filter"] - params_for_search["simple_filter[]"] = Array(params_for_search["simple_filter"]).reject{|v| v.blank?} - params_for_search.delete("simple_filter") + + if params_for_search["aq"] + # Just validate it + params_for_search["aq"] = JSONModel(:advanced_query).from_json(params_for_search["aq"]).to_json end if params_for_search["exclude"] @@ -557,7 +562,7 @@ def handle_transfer(model) helper_method :default_advanced_search_queries def default_advanced_search_queries - [{"i" => 0, "type" => "text"}] + [{"i" => 0, "type" => "text", "comparator" => "contains"}] end @@ -565,7 +570,7 @@ def default_advanced_search_queries def advanced_search_queries return default_advanced_search_queries if !params["advanced"] - indexes = params.keys.collect{|k| k[/^v(?<index>[\d]+)/, "index"]}.compact.sort{|a,b| a.to_i <=> b.to_i} + indexes = params.keys.collect{|k| k[/^f(?<index>[\d]+)/, "index"]}.compact.sort{|a,b| a.to_i <=> b.to_i} return default_advanced_search_queries if indexes.empty? @@ -578,6 +583,11 @@ def advanced_search_queries "type" => params["t#{i}"] } + if query["type"] == "text" + query["comparator"] = params["top#{i}"] + query["empty"] = query["comparator"] == "empty" + end + if query["op"] === "NOT" query["op"] = "AND" query["negated"] = true @@ -585,10 +595,16 @@ def advanced_search_queries if query["type"] == "date" query["comparator"] = params["dop#{i}"] + query["empty"] = query["comparator"] == "empty" end if query["type"] == "boolean" - query["value"] = query["value"] == "true" + query["value"] = query["value"] == "empty" ? "empty" : query["value"] == "true" + query["empty"] = query["value"] == "empty" + end + + if query["type"] == "enum" + query["empty"] = query["value"].blank? end query diff --git a/frontend/app/controllers/archival_objects_controller.rb b/frontend/app/controllers/archival_objects_controller.rb index fe5913bd03..d8ea329660 100644 --- a/frontend/app/controllers/archival_objects_controller.rb +++ b/frontend/app/controllers/archival_objects_controller.rb @@ -47,8 +47,6 @@ def create I18n.t("archival_object._frontend.messages.created_with_parent", JSONModelI18nWrapper.new(:archival_object => @archival_object, :resource => @archival_object['resource']['_resolved'], :parent => @archival_object['parent']['_resolved'])) : I18n.t("archival_object._frontend.messages.created", JSONModelI18nWrapper.new(:archival_object => @archival_object, :resource => @archival_object['resource']['_resolved'])) - @refresh_tree_node = true - if params.has_key?(:plus_one) flash[:success] = success_message else @@ -77,8 +75,6 @@ def update I18n.t("archival_object._frontend.messages.updated", JSONModelI18nWrapper.new(:archival_object => @archival_object, :resource => @archival_object['resource']['_resolved'])) flash.now[:success] = success_message - @refresh_tree_node = true - render_aspace_partial :partial => "edit_inline" }) end @@ -172,6 +168,7 @@ def delete def rde @parent = JSONModel(:archival_object).find(params[:id]) + @resource_uri = @parent['resource']['ref'] @children = ArchivalObjectChildren.new @exceptions = [] @@ -181,6 +178,7 @@ def rde def add_children @parent = JSONModel(:archival_object).find(params[:id]) + @resource_uri = @parent['resource']['ref'] if params[:archival_record_children].blank? or params[:archival_record_children]["children"].blank? diff --git a/frontend/app/controllers/classification_terms_controller.rb b/frontend/app/controllers/classification_terms_controller.rb index b200630eef..f7237b23c5 100644 --- a/frontend/app/controllers/classification_terms_controller.rb +++ b/frontend/app/controllers/classification_terms_controller.rb @@ -37,8 +37,6 @@ def create I18n.t("classification_term._frontend.messages.created_with_parent", JSONModelI18nWrapper.new(:classification_term => @classification_term, :classification => @classification_term['classification']['_resolved'], :parent => @classification_term['parent']['_resolved'])) : I18n.t("classification_term._frontend.messages.created", JSONModelI18nWrapper.new(:classification_term => @classification_term, :classification => @classification_term['classification']['_resolved'])) - @refresh_tree_node = true - if params.has_key?(:plus_one) flash[:success] = success_message else @@ -66,8 +64,6 @@ def update I18n.t("classification_term._frontend.messages.updated", JSONModelI18nWrapper.new(:classification_term => @classification_term, :classification => @classification_term['classification']['_resolved'])) flash.now[:success] = success_message - @refresh_tree_node = true - render_aspace_partial :partial => "edit_inline" }) end diff --git a/frontend/app/controllers/classifications_controller.rb b/frontend/app/controllers/classifications_controller.rb index 3b33a5c2a7..712518d393 100644 --- a/frontend/app/controllers/classifications_controller.rb +++ b/frontend/app/controllers/classifications_controller.rb @@ -1,6 +1,6 @@ class ClassificationsController < ApplicationController - set_access_control "view_repository" => [:index, :show, :tree], + set_access_control "view_repository" => [:index, :show, :tree_root, :tree_node, :tree_waypoint, :node_from_root], "update_classification_record" => [:new, :edit, :create, :update, :accept_children], "delete_classification_record" => [:delete], "manage_repository" => [:defaults, :update_defaults] @@ -82,7 +82,6 @@ def update render_aspace_partial :partial => "edit_inline" }, :on_valid => ->(id){ - @refresh_tree_node = true flash.now[:success] = I18n.t("classification._frontend.messages.updated", JSONModelI18nWrapper.new(:classification => @classification)) render_aspace_partial :partial => "edit_inline" }) @@ -141,6 +140,44 @@ def tree render :json => fetch_tree end + def tree_root + classification_uri = JSONModel(:classification).uri_for(params[:id]) + + render :json => JSONModel::HTTP.get_json("#{classification_uri}/tree/root") + end + + def node_from_root + classification_uri = JSONModel(:classification).uri_for(params[:id]) + + render :json => JSONModel::HTTP.get_json("#{classification_uri}/tree/node_from_root", + 'node_ids[]' => params[:node_ids]) + end + + def tree_node + classification_uri = JSONModel(:classification).uri_for(params[:id]) + node_uri = if !params[:node].blank? + params[:node] + else + nil + end + + render :json => JSONModel::HTTP.get_json("#{classification_uri}/tree/node", + :node_uri => node_uri) + end + + def tree_waypoint + classification_uri = JSONModel(:classification).uri_for(params[:id]) + node_uri = if !params[:node].blank? + params[:node] + else + nil + end + + render :json => JSONModel::HTTP.get_json("#{classification_uri}/tree/waypoint", + :parent_node => node_uri, + :offset => params[:offset]) + end + private diff --git a/frontend/app/controllers/digital_object_components_controller.rb b/frontend/app/controllers/digital_object_components_controller.rb index e2567b83c9..cdd69d987f 100644 --- a/frontend/app/controllers/digital_object_components_controller.rb +++ b/frontend/app/controllers/digital_object_components_controller.rb @@ -52,8 +52,6 @@ def create I18n.t("digital_object_component._frontend.messages.created_with_parent", JSONModelI18nWrapper.new(:digital_object_component => @digital_object_component, :digital_object => @digital_object_component['digital_object']['_resolved'], :parent => @digital_object_component['parent']['_resolved'])) : I18n.t("digital_object_component._frontend.messages.created", JSONModelI18nWrapper.new(:digital_object_component => @digital_object_component, :digital_object => @digital_object_component['digital_object']['_resolved'])) - @refresh_tree_node = true - if params.has_key?(:plus_one) flash[:success] = success_message else @@ -81,8 +79,6 @@ def update I18n.t("digital_object_component._frontend.messages.updated", JSONModelI18nWrapper.new(:digital_object_component => @digital_object_component, :digital_object => digital_object)) flash.now[:success] = success_message - @refresh_tree_node = true - render_aspace_partial :partial => "edit_inline" }) end @@ -153,6 +149,7 @@ def rde flash.clear @parent = JSONModel(:digital_object_component).find(params[:id]) + @digital_object_uri = @parent['digital_object']['ref'] @children = DigitalObjectComponentChildren.new @exceptions = [] @@ -172,6 +169,7 @@ def validate_rows def add_children @parent = JSONModel(:digital_object_component).find(params[:id]) + @digital_object_uri = @parent['digital_object']['ref'] if params[:digital_record_children].blank? or params[:digital_record_children]["children"].blank? diff --git a/frontend/app/controllers/digital_objects_controller.rb b/frontend/app/controllers/digital_objects_controller.rb index 88fca71014..745aeea746 100644 --- a/frontend/app/controllers/digital_objects_controller.rb +++ b/frontend/app/controllers/digital_objects_controller.rb @@ -1,6 +1,6 @@ class DigitalObjectsController < ApplicationController - set_access_control "view_repository" => [:index, :show, :tree], + set_access_control "view_repository" => [:index, :show, :tree_root, :tree_node, :tree_waypoint, :node_from_root], "update_digital_object_record" => [:new, :edit, :create, :update, :publish, :accept_children, :rde, :add_children], "delete_archival_record" => [:delete], "merge_archival_record" => [:merge], @@ -148,7 +148,6 @@ def update render_aspace_partial :partial => "edit_inline" }, :on_valid => ->(id){ - @refresh_tree_node = true flash.now[:success] = I18n.t("digital_object._frontend.messages.updated", JSONModelI18nWrapper.new(:digital_object => @digital_object)) render_aspace_partial :partial => "edit_inline" }) @@ -202,6 +201,7 @@ def rde flash.clear @parent = JSONModel(:digital_object).find(params[:id]) + @digital_object_uri = @parent.uri @children = DigitalObjectChildren.new @exceptions = [] @@ -211,6 +211,7 @@ def rde def add_children @parent = JSONModel(:digital_object).find(params[:id]) + @digital_object_uri = @parent.uri if params[:digital_record_children].blank? or params[:digital_record_children]["children"].blank? @@ -268,6 +269,45 @@ def unsuppress redirect_to(:controller => :digital_objects, :action => :show, :id => params[:id]) end + def tree_root + digital_object_uri = JSONModel(:digital_object).uri_for(params[:id]) + + render :json => JSONModel::HTTP.get_json("#{digital_object_uri}/tree/root") + end + + def node_from_root + digital_object_uri = JSONModel(:digital_object).uri_for(params[:id]) + + render :json => JSONModel::HTTP.get_json("#{digital_object_uri}/tree/node_from_root", + 'node_ids[]' => params[:node_ids]) + end + + def tree_node + digital_object_uri = JSONModel(:digital_object).uri_for(params[:id]) + node_uri = if !params[:node].blank? + params[:node] + else + nil + end + + render :json => JSONModel::HTTP.get_json("#{digital_object_uri}/tree/node", + :node_uri => node_uri) + end + + def tree_waypoint + digital_object_uri = JSONModel(:digital_object).uri_for(params[:id]) + node_uri = if !params[:node].blank? + params[:node] + else + nil + end + + render :json => JSONModel::HTTP.get_json("#{digital_object_uri}/tree/waypoint", + :parent_node => node_uri, + :offset => params[:offset]) + end + + private diff --git a/frontend/app/controllers/exports_controller.rb b/frontend/app/controllers/exports_controller.rb index 76af33957f..0f398b15f5 100644 --- a/frontend/app/controllers/exports_controller.rb +++ b/frontend/app/controllers/exports_controller.rb @@ -67,7 +67,7 @@ def download_export(request_uri, params = {}) respond_to do |format| format.html { - self.response.headers["Content-Type"] ||= meta['mimetype'] + self.response.headers["Content-Type"] = meta['mimetype'] if meta['mimetype'] self.response.headers["Content-Disposition"] = "attachment; filename=#{meta['filename']}" self.response.headers['Last-Modified'] = Time.now.ctime.to_s diff --git a/frontend/app/controllers/jobs_controller.rb b/frontend/app/controllers/jobs_controller.rb index f705428fd3..c9c2ea6422 100644 --- a/frontend/app/controllers/jobs_controller.rb +++ b/frontend/app/controllers/jobs_controller.rb @@ -13,7 +13,6 @@ def index def new @job = JSONModel(:job).new._always_valid! - @job_types = job_types @import_types = import_types @report_data = JSONModel::HTTP::get_json("/reports") @@ -23,16 +22,7 @@ def new def create - job_data = case params['job']['job_type'] - when 'find_and_replace_job' - params['find_and_replace_job'] - when 'print_to_pdf_job' - params['print_to_pdf_job'] - when 'report_job' - params['report_job'] - when 'import_job' - params['import_job'] - end + job_data = params[params['job']['job_type']] # Knock out the _resolved parameter because it's often very large # and clean up the job data to match the schema types. @@ -50,7 +40,6 @@ def create rescue JSONModel::ValidationException => e @exceptions = e.invalid_object._exceptions @job = e.invalid_object - @job_types = job_types @import_types = import_types @job_type = params['job']['job_type'] @@ -131,9 +120,6 @@ def selected_page [Integer(params[:page] || 1), 1].max end - def job_types - Job.available_types.map {|e| [I18n.t("enumerations.job_type.#{e}"), e] unless e.blank? }.compact - end def import_types Job.available_import_types.map {|e| [I18n.t("import_job.import_type_#{e['name']}", default: e['name'] ), e['name']]} diff --git a/frontend/app/controllers/rde_templates_controller.rb b/frontend/app/controllers/rde_templates_controller.rb index de0153f011..b93e798854 100644 --- a/frontend/app/controllers/rde_templates_controller.rb +++ b/frontend/app/controllers/rde_templates_controller.rb @@ -4,7 +4,13 @@ class RdeTemplatesController < ApplicationController set_access_control "view_repository" => [:index, :show] def create - template = JSONModel(:rde_template).from_hash(params['template']) + template_param = params['template'] + + if template_param.respond_to?(:to_unsafe_hash) + template_param = template_param.to_unsafe_hash + end + + template = JSONModel(:rde_template).from_hash(template_param) id = template.save diff --git a/frontend/app/controllers/repositories_controller.rb b/frontend/app/controllers/repositories_controller.rb index 4dd8665879..6e61e667d6 100644 --- a/frontend/app/controllers/repositories_controller.rb +++ b/frontend/app/controllers/repositories_controller.rb @@ -5,7 +5,7 @@ class RepositoriesController < ApplicationController "transfer_repository" => [:transfer, :run_transfer], "delete_repository" => [:delete] - before_filter :refresh_repo_list, :only => [:show, :new] + before_action :refresh_repo_list, :only => [:show, :new] def index diff --git a/frontend/app/controllers/resources_controller.rb b/frontend/app/controllers/resources_controller.rb index 5d12db70f5..6b3cfd7aef 100644 --- a/frontend/app/controllers/resources_controller.rb +++ b/frontend/app/controllers/resources_controller.rb @@ -1,6 +1,6 @@ class ResourcesController < ApplicationController - set_access_control "view_repository" => [:index, :show, :tree, :models_in_graph], + set_access_control "view_repository" => [:index, :show, :tree_root, :tree_node, :tree_waypoint, :node_from_root, :models_in_graph], "update_resource_record" => [:new, :edit, :create, :update, :rde, :add_children, :publish, :accept_children], "delete_archival_record" => [:delete], "merge_archival_record" => [:merge], @@ -100,7 +100,44 @@ def update_defaults end + def tree_root + resource_uri = JSONModel(:resource).uri_for(params[:id]) + render :json => pass_through_json("#{resource_uri}/tree/root") + end + + def node_from_root + resource_uri = JSONModel(:resource).uri_for(params[:id]) + + render :json => pass_through_json("#{resource_uri}/tree/node_from_root", + 'node_ids[]' => params[:node_ids]) + end + + def tree_node + resource_uri = JSONModel(:resource).uri_for(params[:id]) + node_uri = if !params[:node].blank? + params[:node] + else + nil + end + + render :json => pass_through_json("#{resource_uri}/tree/node", + :node_uri => node_uri) + end + + def tree_waypoint + resource_uri = JSONModel(:resource).uri_for(params[:id]) + node_uri = if !params[:node].blank? + params[:node] + else + nil + end + + render :json => pass_through_json("#{resource_uri}/tree/waypoint", + :parent_node => node_uri, + :offset => params[:offset]) + + end def transfer begin @@ -156,7 +193,6 @@ def update render_aspace_partial :partial => "edit_inline" }, :on_valid => ->(id){ - @refresh_tree_node = true flash.now[:success] = I18n.t("resource._frontend.messages.updated", JSONModelI18nWrapper.new(:resource => @resource)) render_aspace_partial :partial => "edit_inline" }) @@ -176,6 +212,7 @@ def rde flash.clear @parent = Resource.find(params[:id]) + @resource_uri = @parent.uri @children = ResourceChildren.new @exceptions = [] @@ -185,6 +222,7 @@ def rde def add_children @parent = Resource.find(params[:id]) + @resource_uri = @parent.uri if params[:archival_record_children].blank? or params[:archival_record_children]["children"].blank? @@ -252,11 +290,6 @@ def merge end - def tree - render :json => fetch_tree - end - - def suppress resource = JSONModel(:resource).find(params[:id]) resource.set_suppressed(true) @@ -286,70 +319,19 @@ def models_in_graph private - def fetch_tree - flash.keep # keep the flash... just in case this fires before the form is loaded - - tree = [] - limit_to = if params[:node_uri] && !params[:node_uri].include?("/resources/") - params[:node_uri] - else - "root" - end + def pass_through_json(uri, params = {}) + json = "{}" - if !params[:hash].blank? - node_id = params[:hash].sub("tree::", "").sub("#", "") - if node_id.starts_with?("resource") - limit_to = "root" - elsif node_id.starts_with?("archival_object") - limit_to = JSONModel(:archival_object).uri_for(node_id.sub("archival_object_", "").to_i) - end + JSONModel::HTTP.stream(uri, params) do |response| + json = response.body end - tree = JSONModel(:resource_tree).find(nil, :resource_id => params[:id], :limit_to => limit_to).to_hash(:validated) - - prepare_tree_nodes(tree) do |node| - - node['text'] = node['title'] - node['level'] = I18n.t("enumerations.archival_record_level.#{node['level']}", :default => node['level']) - node['instance_types'] = node['instance_types'].map{|instance_type| I18n.t("enumerations.instance_instance_type.#{instance_type}", :default => instance_type)} - node['containers'].each{|container| - container["type_1"] = I18n.t("enumerations.container_type.#{container["type_1"]}", :default => container["type_1"]) if container["type_1"] - container["type_2"] = I18n.t("enumerations.container_type.#{container["type_2"]}", :default => container["type_2"]) if container["type_2"] - container["type_3"] = I18n.t("enumerations.container_type.#{container["type_3"]}", :default => container["type_3"]) if container["type_3"] - } - node_db_id = node['id'] - - node['id'] = "#{node["node_type"]}_#{node["id"]}" - - if node['has_children'] && node['children'].empty? - node['children'] = true - end - - node['type'] = node['node_type'] - - node['li_attr'] = { - "data-uri" => node['record_uri'], - "data-id" => node_db_id, - "rel" => node['node_type'] - } - node['a_attr'] = { - "href" => "#tree::#{node['id']}", - "title" => node["title"] - } - - if node['node_type'] == 'resource' || node['record_uri'] == limit_to -# node['state'] = {'opened' => true} - end - - end - - tree - + json end - # refactoring note: suspiciously similar to accessions_controller.rb +# refactoring note: suspiciously similar to accessions_controller.rb def fetch_resolved(id) resource = JSONModel(:resource).find(id, find_opts) diff --git a/frontend/app/controllers/search_controller.rb b/frontend/app/controllers/search_controller.rb index 98a77c5ac1..6800bc0683 100644 --- a/frontend/app/controllers/search_controller.rb +++ b/frontend/app/controllers/search_controller.rb @@ -9,10 +9,12 @@ class SearchController < ApplicationController def advanced_search criteria = params_for_backend_search - queries = advanced_search_queries.reject{|field| field["value"].nil? || field["value"] == ""} + queries = advanced_search_queries.reject{|field| + (field["value"].nil? || field["value"] == "") && !field["empty"] + } if not queries.empty? - criteria["aq"] = AdvancedQueryBuilder.new(queries, :staff).build_query.to_json + criteria["aq"] = AdvancedQueryBuilder.build_query_from_form(queries).to_json criteria['facet[]'] = SearchResultData.BASE_FACETS end diff --git a/frontend/app/controllers/top_containers_controller.rb b/frontend/app/controllers/top_containers_controller.rb index 69bc13891d..8942c43af3 100644 --- a/frontend/app/controllers/top_containers_controller.rb +++ b/frontend/app/controllers/top_containers_controller.rb @@ -1,5 +1,6 @@ require 'uri' require 'barcode_check' +require 'advanced_query_builder' class TopContainersController < ApplicationController @@ -216,7 +217,7 @@ def search_filter_for(uri) return {} if uri.blank? return { - "filter_term[]" => [{"collection_uri_u_sstr" => uri}.to_json] + 'filter' => AdvancedQueryBuilder.new.and('collection_uri_u_sstr', uri, 'text', true).build.to_json } end @@ -226,34 +227,53 @@ def perform_search 'type[]' => ['top_container'] }) - filter_terms = [] - simple_filters = [] + builder = AdvancedQueryBuilder.new - filter_terms.push({'collection_uri_u_sstr' => params['collection_resource']['ref']}.to_json) if params['collection_resource'] - filter_terms.push({'collection_uri_u_sstr' => params['collection_accession']['ref']}.to_json) if params['collection_accession'] + if params['collection_resource'] + builder.and('collection_uri_u_sstr', params['collection_resource']['ref'], 'text', literal = true) + end + + if params['collection_accession'] + builder.and('collection_uri_u_sstr', params['collection_accession']['ref'], 'text', literal = true) + end + + if params['container_profile'] + builder.and('container_profile_uri_u_sstr', params['container_profile']['ref'], 'text', literal = true) + end + + if params['location'] + builder.and('location_uri_u_sstr', params['location']['ref'], 'text', literal = true) + end - filter_terms.push({'container_profile_uri_u_sstr' => params['container_profile']['ref']}.to_json) if params['container_profile'] - filter_terms.push({'location_uri_u_sstr' => params['location']['ref']}.to_json) if params['location'] unless params['exported'].blank? - filter_terms.push({'exported_u_sbool' => (params['exported'] == "yes" ? true : false)}.to_json) + builder.and('exported_u_sbool', + (params['exported'] == "yes" ? 'true' : 'false'), + 'boolean') end + unless params['empty'].blank? - filter_terms.push({'empty_u_sbool' => (params['empty'] == "yes" ? true : false)}.to_json) + builder.and('empty_u_sbool', (params['empty'] == "yes" ? 'true' : 'false'), 'boolean') end + unless params['barcodes'].blank? - simple_filters.push(ASUtils.wrap(params['barcodes'].split(" ")).map{|barcode| - "barcode_u_sstr:#{barcode}" - }.join(" OR ")) + barcode_query = AdvancedQueryBuilder.new + + ASUtils.wrap(params['barcodes'].split(" ")).each do |barcode| + barcode_query.or('barcode_u_sstr', barcode) + end + + unless barcode_query.empty? + builder.and(barcode_query) + end end - if simple_filters.empty? && filter_terms.empty? && params['q'].blank? + if builder.empty? && params['q'].blank? raise MissingFilterException.new end - unless filter_terms.empty? && simple_filters.empty? + unless builder.empty? search_params = search_params.merge({ - "filter_term[]" => filter_terms, - "simple_filter[]" => simple_filters + "filter" => builder.build.to_json, }) end diff --git a/frontend/app/controllers/update_monitor_controller.rb b/frontend/app/controllers/update_monitor_controller.rb index 8e469dc97b..fb756d6368 100644 --- a/frontend/app/controllers/update_monitor_controller.rb +++ b/frontend/app/controllers/update_monitor_controller.rb @@ -4,7 +4,7 @@ class UpdateMonitorController < ApplicationController # Turn off CSRF checking for this endpoint since we won't send through a # token, and the failed check blats out the session, which we need. - skip_before_filter :verify_authenticity_token, :only => [:poll] + skip_before_action :verify_authenticity_token, :only => [:poll] def poll uri = params[:uri] diff --git a/frontend/app/controllers/users_controller.rb b/frontend/app/controllers/users_controller.rb index c7b2ec2ecd..329f7f92f7 100644 --- a/frontend/app/controllers/users_controller.rb +++ b/frontend/app/controllers/users_controller.rb @@ -4,9 +4,9 @@ class UsersController < ApplicationController "manage_repository" => [:manage_access, :edit_groups, :update_groups, :complete], :public => [:new, :create] - before_filter :account_self_service, :only => [:new, :create] - before_filter :user_needs_to_be_a_user_manager_or_new_user, :only => [:new, :create] - before_filter :user_needs_to_be_a_user, :only => [:show] + before_action :account_self_service, :only => [:new, :create] + before_action :user_needs_to_be_a_user_manager_or_new_user, :only => [:new, :create] + before_action :user_needs_to_be_a_user, :only => [:show] def index diff --git a/frontend/app/helpers/application_helper.rb b/frontend/app/helpers/application_helper.rb index 02b7dc9534..6128a44416 100644 --- a/frontend/app/helpers/application_helper.rb +++ b/frontend/app/helpers/application_helper.rb @@ -5,22 +5,22 @@ module ApplicationHelper def include_controller_js scripts = "" - scripts += javascript_include_tag "#{controller.controller_name}" if File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.js") || File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.js.erb") + scripts += javascript_include_tag "#{controller.controller_name}" if File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.js") || File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.js.erb") - scripts += javascript_include_tag "#{controller.controller_name}.#{controller.action_name}" if File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.#{controller.action_name}.js") || File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.#{controller.action_name}.js.erb") + scripts += javascript_include_tag "#{controller.controller_name}.#{controller.action_name}" if File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.#{controller.action_name}.js") || File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.#{controller.action_name}.js.erb") if ["new", "create", "edit", "update"].include?(controller.action_name) - scripts += javascript_include_tag "#{controller.controller_name}.crud" if File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.crud.js") || File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.crud.js.erb") + scripts += javascript_include_tag "#{controller.controller_name}.crud" if File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.crud.js") || File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.crud.js.erb") end if ["batch_create"].include?(controller.action_name) - scripts += javascript_include_tag "#{controller.controller_name}.batch" if File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.batch.js") || File.exists?("#{Rails.root}/app/assets/javascripts/#{controller_name}.batch.js.erb") + scripts += javascript_include_tag "#{controller.controller_name}.batch" if File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.batch.js") || File.exist?("#{Rails.root}/app/assets/javascripts/#{controller_name}.batch.js.erb") end if ["defaults", "update_defaults"].include?(controller.action_name) ctrl_name = controller.controller_name == 'archival_objects' ? 'resources' : controller.controller_name - scripts += javascript_include_tag "#{ctrl_name}.crud" if File.exists?("#{Rails.root}/app/assets/javascripts/#{ctrl_name}.crud.js") || File.exists?("#{Rails.root}/app/assets/javascripts/#{ctrl_name}.crud.js.erb") + scripts += javascript_include_tag "#{ctrl_name}.crud" if File.exist?("#{Rails.root}/app/assets/javascripts/#{ctrl_name}.crud.js") || File.exist?("#{Rails.root}/app/assets/javascripts/#{ctrl_name}.crud.js.erb") end @@ -28,12 +28,24 @@ def include_controller_js end def include_theme_css - css = "" - css += stylesheet_link_tag("themes/#{ArchivesSpace::Application.config.frontend_theme}/bootstrap", :media => "all") - css += stylesheet_link_tag("themes/#{ArchivesSpace::Application.config.frontend_theme}/application", :media => "all") - css.html_safe + begin + css = "" + css += stylesheet_link_tag("themes/#{ArchivesSpace::Application.config.frontend_theme}/bootstrap", :media => "all") + css += stylesheet_link_tag("themes/#{ArchivesSpace::Application.config.frontend_theme}/application", :media => "all") + css.html_safe + rescue + # On app startup in dev mode, the above call triggers the LESS stylesheets + # to compile, and there seems to be a problem with two threads doing this + # concurrently. If things go badly, just retry. + + Rails.logger.warn("Retrying include_theme_css: #{$!}") + + sleep 1 + retry + end end + def set_title(title) @title = title end @@ -137,6 +149,11 @@ def current_repo end + def job_types + MemoryLeak::Resources.get(:job_types) + end + + def current_user session[:user] end @@ -306,4 +323,15 @@ def export_csv(search_data) end + # Merge new_params into params and generate a link. + # + # Intended to avoid security issues associated with passing user-generated + # `params` as the `opts` for link_to (which allows them to set the host, + # controller, etc.) + def link_to_merge_params(label, new_params, html_options = {}) + link_to(label, + url_for + "?" + URI.encode_www_form(params.except(:controller, :action, :format).merge(new_params)), + html_options) + end + end diff --git a/frontend/app/helpers/aspace_form_helper.rb b/frontend/app/helpers/aspace_form_helper.rb index a386b36b1b..3728faaa74 100644 --- a/frontend/app/helpers/aspace_form_helper.rb +++ b/frontend/app/helpers/aspace_form_helper.rb @@ -283,7 +283,6 @@ def select(name, options, opts = {}) @forms.select_tag(path(name), @forms.options_for_select(options, obj[name] || default_for(name) || opts[:default]), {:id => id_for(name)}.merge!(opts)) end - def textarea(name = nil, value = "", opts = {}) options = {:id => id_for(name), :rows => 3} diff --git a/frontend/app/helpers/enumeration_helper.rb b/frontend/app/helpers/enumeration_helper.rb index 24cd3b98da..2709c6bec2 100644 --- a/frontend/app/helpers/enumeration_helper.rb +++ b/frontend/app/helpers/enumeration_helper.rb @@ -1,9 +1,15 @@ +require 'advanced_query_builder' + module EnumerationHelper - def enumeration_simple_filter_params( relationships, value ) - pattern = %r{(\+|\-|\&\&|\|\||\!|\(|\)|\{|\}|\[|\]|\^|\"|\~|\*|\?|\ |\:|\\)} - value.gsub!(pattern) { |match| '\\' + match } - relationships.map { |rel| "#{rel}_enum_s:#{value}" }.join(" OR ") + def enumeration_advanced_query(relationships, value) + query = AdvancedQueryBuilder.new + + relationships.each do |rel| + query.or("#{rel}_enum_s", value) + end + + query.build.to_json end end diff --git a/frontend/app/helpers/search_helper.rb b/frontend/app/helpers/search_helper.rb index bc46d58559..398665812f 100644 --- a/frontend/app/helpers/search_helper.rb +++ b/frontend/app/helpers/search_helper.rb @@ -6,8 +6,6 @@ def build_search_params(opts = {}) search_params["filter_term"] = Array(opts["filter_term"] || params["filter_term"]).clone search_params["filter_term"].concat(Array(opts["add_filter_term"])) if opts["add_filter_term"] search_params["filter_term"] = search_params["filter_term"].reject{|f| Array(opts["remove_filter_term"]).include?(f)} if opts["remove_filter_term"] - - search_params["simple_filters"] = Array(opts["simple_filters"] || params["simple_filters"]).clone if params["multiplicity"] search_params["multiplicity"] = params["multiplicity"] @@ -49,7 +47,7 @@ def build_search_params(opts = {}) if params["advanced"] search_params["advanced"] = params["advanced"] params.keys.each do |param_key| - ["op", "f", "v", "dop", "t"].each do |adv_search_prefix| + ["op", "f", "v", "dop", "t", "top"].each do |adv_search_prefix| if param_key =~ /^#{adv_search_prefix}\d+/ search_params[param_key] = params[param_key] end diff --git a/frontend/app/models/search.rb b/frontend/app/models/search.rb index 66f2ff5c8c..6a3a21d9eb 100644 --- a/frontend/app/models/search.rb +++ b/frontend/app/models/search.rb @@ -1,4 +1,5 @@ require 'search_result_data' +require 'advanced_query_builder' class Search @@ -10,6 +11,8 @@ def self.for_type(repo_id, type, criteria) def self.all(repo_id, criteria) + build_filters(criteria) + criteria["page"] = 1 if not criteria.has_key?("page") search_data = JSONModel::HTTP::get_json("/repositories/#{repo_id}/search", criteria) @@ -20,6 +23,8 @@ def self.all(repo_id, criteria) def self.global(criteria, type) + build_filters(criteria) + criteria["page"] = 1 if not criteria.has_key?("page") search_data = JSONModel::HTTP::get_json("/search/#{type}", criteria) @@ -27,4 +32,20 @@ def self.global(criteria, type) search_data[:type] = type SearchResultData.new(search_data) end + + private + + def self.build_filters(criteria) + queries = AdvancedQueryBuilder.new + + Array(criteria['filter_term[]']).each do |json_filter| + filter = ASUtils.json_parse(json_filter) + queries.and(filter.keys[0], filter.values[0]) + end + + unless queries.empty? + criteria['filter'] = queries.build.to_json + end + end + end diff --git a/frontend/app/views/accessions/_linker.html.erb b/frontend/app/views/accessions/_linker.html.erb index 491c77473b..353a920501 100644 --- a/frontend/app/views/accessions/_linker.html.erb +++ b/frontend/app/views/accessions/_linker.html.erb @@ -40,8 +40,8 @@ data-exclude='<%= exclude_ids.to_json %>' /> - <div class="btn-group dropdown pull-right"> - <a class="btn dropdown-toggle last" data-toggle="dropdown" href="javascript:void(0);"><span class="caret"></span></a> + <div class="input-group-btn"> + <a class="btn btn-default dropdown-toggle last" data-toggle="dropdown" href="javascript:void(0);"><span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="javascript:void(0);" class="linker-browse-btn"><%= I18n.t("actions.browse") %></a></li> </ul> diff --git a/frontend/app/views/archival_objects/_edit_inline.html.erb b/frontend/app/views/archival_objects/_edit_inline.html.erb index a8413ff859..76df45fe83 100644 --- a/frontend/app/views/archival_objects/_edit_inline.html.erb +++ b/frontend/app/views/archival_objects/_edit_inline.html.erb @@ -15,41 +15,5 @@ </div> </div> </div> - <% if @refresh_tree_node - node_data = { - 'id' => @archival_object.id, - 'uri' => @archival_object.uri, - 'title' => @archival_object.display_string, - 'level' => @archival_object.level==='otherlevel' ? @archival_object.other_level : I18n.t("enumerations.archival_record_level.#{@archival_object.level}", :default => @archival_object.level), - 'jsonmodel_type' => @archival_object.jsonmodel_type, - 'node_type' => @archival_object.jsonmodel_type, - 'instance_types' => @archival_object.instances.map{|instance| I18n.t("enumerations.instance_instance_type.#{instance['instance_type']}", :default => instance['instance_type'])}, - 'containers' => @archival_object.to_hash["instances"].map{|instance| instance["sub_container"]}.compact.map {|sub_container| - properties = {} - if sub_container["top_container"] - top_container = sub_container["top_container"]["_resolved"] - # if _resolved comes from search result, then need to parse JSON - if top_container["json"] - top_container = ASUtils.json_parse(top_container["json"]) - end - properties["type_1"] = "Container" - properties["indicator_1"] = top_container["indicator"] - if top_container["barcode"] - properties["indicator_1"] += " [#{top_container["barcode"]}]" - end - end - properties["type_2"] = I18n.t("enumerations.container_type.#{sub_container["type_2"]}", :default => sub_container["type_2"]) - properties["indicator_2"] =sub_container["indicator_2"] - properties["type_3"] = I18n.t("enumerations.container_type.#{sub_container["type_3"]}", :default => sub_container["type_3"]) - properties["indicator_3"] =sub_container["indicator_3"] - properties - }, - 'replace_new_node' => controller.action_name === 'create' - } - %> - <script> - AS.refreshTreeNode(<%= node_data.to_json.html_safe %>); - </script> - <% end %> <% end %> <% end %> diff --git a/frontend/app/views/archival_objects/_form_container.html.erb b/frontend/app/views/archival_objects/_form_container.html.erb index de106671ea..2531eae054 100644 --- a/frontend/app/views/archival_objects/_form_container.html.erb +++ b/frontend/app/views/archival_objects/_form_container.html.erb @@ -10,8 +10,9 @@ <%= form.hidden_input "parent", nil, {"data-base-name" => "archival_object[parent]", "class" => "hidden-parent-uri"} %> <%= form.hidden_input "resource" %> - <%= form.hidden_input "position" %> + <%= hidden_field_tag "id", @archival_object.id %> + <%= hidden_field_tag "uri", @archival_object.uri %> <% define_template("archival_object", jsonmodel_definition(:archival_object)) do |form| %> <section id="basic_information"> @@ -34,8 +35,8 @@ <%= form.label_and_textfield "other_level", :required => true %> <%= form.label_and_select "language", form.possible_options_for("language", true) %> <div class="form-group"> - <%= form.label "publish" %> - <div class="controls"> + <%= form.label "publish", :class => 'control-label col-sm-2' %> + <div class="checkbox col-sm-9"> <%= form.checkbox "publish", {}, user_prefs["publish"] %> <% if form.obj["has_unpublished_ancestor"] %> <span class="help-inline"><span class="text-info"><%= I18n.t("archival_object._frontend.messages.has_unpublished_ancestor") %></span></span> diff --git a/frontend/app/views/archival_objects/_rde_templates.html.erb b/frontend/app/views/archival_objects/_rde_templates.html.erb index 044956ef7d..1a19e6b4af 100644 --- a/frontend/app/views/archival_objects/_rde_templates.html.erb +++ b/frontend/app/views/archival_objects/_rde_templates.html.erb @@ -97,6 +97,8 @@ </div> <div class="error-summary-list"></div> </div> + + <%= form.hidden_input "resource[ref]", @resource_uri %> </td> <td data-col="colLevel" class="form-group"><%= form.select "level", form.possible_options_for("level", true) %></td> <td data-col="colOtherLevel" class="form-group"><%= form.textfield "other_level" %></td> diff --git a/frontend/app/views/archival_objects/_toolbar.html.erb b/frontend/app/views/archival_objects/_toolbar.html.erb index a12078323b..669bc2d97d 100644 --- a/frontend/app/views/archival_objects/_toolbar.html.erb +++ b/frontend/app/views/archival_objects/_toolbar.html.erb @@ -1,7 +1,7 @@ <% if user_can?('update_resource_record') %> <%= render_aspace_partial(:partial => '/shared/component_toolbar', :locals => { - :parent_link => {:controller => :resources, :action => :edit, :id => JSONModel(:resource).id_for(@archival_object.resource["ref"]), :anchor => "archival_object_#{@archival_object.id}"}, + :parent_link => {:controller => :resources, :action => :edit, :id => JSONModel(:resource).id_for(@archival_object.resource["ref"]), :anchor => "tree::archival_object_#{@archival_object.id}"}, :record => @archival_object, :show_delete => user_can?('delete_archival_record') }) diff --git a/frontend/app/views/classification_terms/_edit_inline.html.erb b/frontend/app/views/classification_terms/_edit_inline.html.erb index 4ab99a6121..b724c6032c 100644 --- a/frontend/app/views/classification_terms/_edit_inline.html.erb +++ b/frontend/app/views/classification_terms/_edit_inline.html.erb @@ -15,20 +15,5 @@ </div> </div> </div> - <% if @refresh_tree_node - node_data = { - 'id' => @classification_term.id, - 'uri' => @classification_term.uri, - 'title' => @classification_term.title, - 'identifier' => @classification_term.identifier, - 'jsonmodel_type' => @classification_term.jsonmodel_type, - 'node_type' => @classification_term.jsonmodel_type, - 'replace_new_node' => controller.action_name === 'create' - } - %> - <script> - AS.refreshTreeNode(<%= node_data.to_json.html_safe %>); - </script> - <% end %> <% end %> <% end %> diff --git a/frontend/app/views/classification_terms/_form_container.html.erb b/frontend/app/views/classification_terms/_form_container.html.erb index 9e83c3afae..a14f75e37f 100644 --- a/frontend/app/views/classification_terms/_form_container.html.erb +++ b/frontend/app/views/classification_terms/_form_container.html.erb @@ -11,8 +11,9 @@ <%= form.hidden_input "parent", nil, {"data-base-name" => "classification_term[parent]", "class" => "hidden-parent-uri"} %> <%= form.hidden_input "classification" %> - <%= form.hidden_input "position" %> + <%= hidden_field_tag "id", @classification_term.id %> + <%= hidden_field_tag "uri", @classification_term.uri %> <% define_template("classification_term", jsonmodel_definition(:classification_term)) do |form| %> <section id="basic_information"> diff --git a/frontend/app/views/classifications/_edit_inline.html.erb b/frontend/app/views/classifications/_edit_inline.html.erb index bda5cf1a77..132f3e16aa 100644 --- a/frontend/app/views/classifications/_edit_inline.html.erb +++ b/frontend/app/views/classifications/_edit_inline.html.erb @@ -15,21 +15,5 @@ </div> </div> </div> - - <% if @refresh_tree_node - node_data = { - 'id' => @classification.id, - 'uri' => @classification.uri, - 'title' => @classification.title, - 'identifier' => @classification.identifier, - 'jsonmodel_type' => @classification.jsonmodel_type, - 'node_type' => @classification.jsonmodel_type, - 'replace_new_node' => false - } - %> - <script> - AS.refreshTreeNode(<%= node_data.to_json.html_safe %>); - </script> - <% end %> <% end %> <% end %> diff --git a/frontend/app/views/classifications/_form_container.html.erb b/frontend/app/views/classifications/_form_container.html.erb index 5e56e364c1..b30bef344b 100644 --- a/frontend/app/views/classifications/_form_container.html.erb +++ b/frontend/app/views/classifications/_form_container.html.erb @@ -7,6 +7,7 @@ <%= render_aspace_partial :partial => "shared/form_messages", :locals => {:form => form} %> <%= hidden_field_tag "id", @classification.id %> +<%= hidden_field_tag "uri", @classification.uri %> <fieldset> <% define_template("classification", jsonmodel_definition(:classification)) do |form| %> diff --git a/frontend/app/views/classifications/edit.html.erb b/frontend/app/views/classifications/edit.html.erb index 393a4e9918..2e3946c9bf 100644 --- a/frontend/app/views/classifications/edit.html.erb +++ b/frontend/app/views/classifications/edit.html.erb @@ -1,9 +1,3 @@ <%= setup_context :object => @classification %> -<div class="row" xmlns="http://www.w3.org/1999/html"> - <div class="col-md-12"> - <%= render_aspace_partial :partial => "shared/tree", :locals => {:root_record => @classification} %> - </div> -</div> - -<div id="object_container"></div> +<%= render_aspace_partial :partial => "shared/largetree", :locals => {:root_record => @classification, :read_only => false} %> diff --git a/frontend/app/views/classifications/show.html.erb b/frontend/app/views/classifications/show.html.erb index 3df6a0774b..fd9379da83 100644 --- a/frontend/app/views/classifications/show.html.erb +++ b/frontend/app/views/classifications/show.html.erb @@ -1,9 +1,3 @@ <%= setup_context :object => @classification %> -<div class="row"> - <div class="col-md-12"> - <%= render_aspace_partial :partial => "shared/tree", :locals => {:root_record => @classification, :read_only => true} %> - </div> -</div> - -<div id="object_container"></div> +<%= render_aspace_partial :partial => "shared/largetree", :locals => {:root_record => @classification, :read_only => true} %> diff --git a/frontend/app/views/digital_object_components/_edit_inline.html.erb b/frontend/app/views/digital_object_components/_edit_inline.html.erb index 0daa521d3d..29d474bd5e 100644 --- a/frontend/app/views/digital_object_components/_edit_inline.html.erb +++ b/frontend/app/views/digital_object_components/_edit_inline.html.erb @@ -15,21 +15,5 @@ </div> </div> </div> - <% if @refresh_tree_node - node_data = { - 'id' => @digital_object_component.id, - 'uri' => @digital_object_component.uri, - 'title' => @digital_object_component.display_string, - 'level' => false, - 'jsonmodel_type' => @digital_object_component.jsonmodel_type, - 'node_type' => @digital_object_component.jsonmodel_type, - 'file_versions' => @digital_object_component.file_versions, - 'replace_new_node' => controller.action_name === 'create' - } - %> - <script> - AS.refreshTreeNode(<%= node_data.to_json.html_safe %>); - </script> - <% end %> <% end %> <% end %> diff --git a/frontend/app/views/digital_object_components/_form_container.html.erb b/frontend/app/views/digital_object_components/_form_container.html.erb index e36a96d4f1..7344c71410 100644 --- a/frontend/app/views/digital_object_components/_form_container.html.erb +++ b/frontend/app/views/digital_object_components/_form_container.html.erb @@ -11,8 +11,9 @@ <%= form.hidden_input "parent", nil, {"data-base-name" => "digital_object_component[parent]", "class" => "hidden-parent-uri"} %> <%= form.hidden_input "digital_object" %> - <%= form.hidden_input "position" %> + <%= hidden_field_tag "id", @digital_object_component.id %> + <%= hidden_field_tag "uri", @digital_object_component.uri %> <% define_template("digital_object_component", jsonmodel_definition(:digital_object_component)) do |form| %> <section id="basic_information"> @@ -24,8 +25,8 @@ <%= form.label_and_textarea "title", :required => :conditionally %> <%= form.label_and_textfield "component_id" %> <div class="form-group"> - <%= form.label "publish" %> - <div class="controls"> + <%= form.label "publish", :class => 'control-label col-sm-2' %> + <div class="checkbox col-sm-9"> <%= form.checkbox "publish", {}, user_prefs["publish"] %> <% if form.obj["has_unpublished_ancestor"] %> <span class="help-inline"><span class="text-info"><%= I18n.t("digital_object_component._frontend.messages.has_unpublished_ancestor") %></span></span> diff --git a/frontend/app/views/digital_object_components/_rde_templates.html.erb b/frontend/app/views/digital_object_components/_rde_templates.html.erb index 89bb9fcacd..8ff52df3f6 100644 --- a/frontend/app/views/digital_object_components/_rde_templates.html.erb +++ b/frontend/app/views/digital_object_components/_rde_templates.html.erb @@ -100,6 +100,8 @@ </div> <div class="error-summary-list"></div> </div> + + <%= form.hidden_input "digital_object[ref]", @digital_object_uri %> </td> <td data-col="colLabel" class="form-group"><%= form.textfield "label" %></td> diff --git a/frontend/app/views/digital_object_components/_toolbar.html.erb b/frontend/app/views/digital_object_components/_toolbar.html.erb index d0b577be33..36497e6920 100644 --- a/frontend/app/views/digital_object_components/_toolbar.html.erb +++ b/frontend/app/views/digital_object_components/_toolbar.html.erb @@ -1,7 +1,7 @@ <% if user_can?('update_digital_object_record') %> <%= render_aspace_partial(:partial => '/shared/component_toolbar', :locals => { - :parent_link => {:controller => :digital_objects, :action => :edit, :id => JSONModel(:digital_object).id_for(@digital_object_component.digital_object["ref"]), :anchor => "digital_object_component_#{@digital_object_component.id}"}, + :parent_link => {:controller => :digital_objects, :action => :edit, :id => JSONModel(:digital_object).id_for(@digital_object_component.digital_object["ref"]), :anchor => "tree::digital_object_component_#{@digital_object_component.id}"}, :record => @digital_object_component, :show_delete => user_can?('delete_archival_record') }) diff --git a/frontend/app/views/digital_objects/_edit_inline.html.erb b/frontend/app/views/digital_objects/_edit_inline.html.erb index 2477d06d70..ec17a68e5e 100644 --- a/frontend/app/views/digital_objects/_edit_inline.html.erb +++ b/frontend/app/views/digital_objects/_edit_inline.html.erb @@ -20,22 +20,5 @@ </div> </div> </div> - <% if @refresh_tree_node - node_data = { - 'id' => @digital_object.id, - 'uri' => @digital_object.uri, - 'title' => @digital_object.title, - 'level' => I18n.t("enumerations.digital_object_level.#{@digital_object.level}", :default => @digital_object.level), - 'jsonmodel_type' => @digital_object.jsonmodel_type, - 'node_type' => @digital_object.jsonmodel_type, - 'file_versions' => @digital_object.file_versions, - 'digital_object_type' => I18n.t("enumerations.digital_object_digital_object_type.#{@digital_object.digital_object_type}", :default => @digital_object.digital_object_type), - 'replace_new_node' => false - } - %> - <script> - AS.refreshTreeNode(<%= node_data.to_json.html_safe %>); - </script> - <% end %> <% end %> <% end %> diff --git a/frontend/app/views/digital_objects/_form_container.html.erb b/frontend/app/views/digital_objects/_form_container.html.erb index 732cbf0ec4..b0aee2ca1b 100644 --- a/frontend/app/views/digital_objects/_form_container.html.erb +++ b/frontend/app/views/digital_objects/_form_container.html.erb @@ -2,6 +2,8 @@ <fieldset> + <%= hidden_field_tag "uri", @digital_object.uri %> + <% define_template "digital_object", jsonmodel_definition(:digital_object) do |form| %> <section id="basic_information"> <h3> diff --git a/frontend/app/views/digital_objects/edit.html.erb b/frontend/app/views/digital_objects/edit.html.erb index 9f1a4ca65e..7f5405d04d 100644 --- a/frontend/app/views/digital_objects/edit.html.erb +++ b/frontend/app/views/digital_objects/edit.html.erb @@ -1,9 +1,3 @@ <%= setup_context :object => @digital_object %> -<div class="row"> - <div class="col-md-12"> - <%= render_aspace_partial :partial => "shared/tree", :locals => {:root_record => @digital_object} %> - </div> -</div> - -<div id="object_container"></div> +<%= render_aspace_partial :partial => "shared/largetree", :locals => {:root_record => @digital_object, :read_only => false} %> diff --git a/frontend/app/views/digital_objects/show.html.erb b/frontend/app/views/digital_objects/show.html.erb index fea5255cbd..f0609b0ec2 100644 --- a/frontend/app/views/digital_objects/show.html.erb +++ b/frontend/app/views/digital_objects/show.html.erb @@ -1,11 +1,4 @@ <%= setup_context :object => @digital_object %> <%= render_aspace_partial :partial => "transfer/transfer_failures", :locals => { :transfer_errors => @transfer_errors } %> - -<div class="row"> - <div class="col-md-12"> - <%= render_aspace_partial :partial => "shared/tree", :locals => {:root_record => @digital_object, :read_only => true} %> - </div> -</div> - -<div id="object_container"></div> +<%= render_aspace_partial :partial => "shared/largetree", :locals => {:root_record => @digital_object, :read_only => true} %> diff --git a/frontend/app/views/enumerations/_list.html.erb b/frontend/app/views/enumerations/_list.html.erb index c5ac5e4970..bf13aaf7c0 100644 --- a/frontend/app/views/enumerations/_list.html.erb +++ b/frontend/app/views/enumerations/_list.html.erb @@ -74,8 +74,8 @@ </td> <td> <% if @enumeration['relationships'].length > 0 and @enumeration['name'] != 'job_type' %> - <div class='enum-value-search' data-url="<%= url_for({:controller => :search, :action => :do_search, :simple_filter => - enumeration_simple_filter_params(@enumeration['relationships'], enum_value['value'] ) }) %>"><i class='spinner'></i> + <div class='enum-value-search' data-url="<%= url_for({:controller => :search, :action => :do_search, :aq => + enumeration_advanced_query(@enumeration['relationships'], enum_value['value'] ) }) %>"><i class='spinner'></i> </div> <% end %> </td> diff --git a/frontend/app/views/jobs/_form.html.erb b/frontend/app/views/jobs/_form.html.erb index 35a172bb84..f28d4459b6 100644 --- a/frontend/app/views/jobs/_form.html.erb +++ b/frontend/app/views/jobs/_form.html.erb @@ -50,18 +50,15 @@ <% end %> <% define_template("job", jsonmodel_definition(:job)) do |form| %> - <section> - <fieldset> - <%= form.label_and_select "job_type", @job_types, :field_opts => { :default => @job_type } %> - </fieldset> - </section> + + <input id="job_type" name="job[job_type]" type="hidden" value="<%= params['job_type'] %>"> + <div id="job_form_messages"> <hr/> <div class="alert alert-info" id="noImportTypeSelected"><%= I18n.t("job._frontend.messages.import_type_missing") %></div> <hr/> </div> - <div id="job_type_fields"></div> <% end %> @@ -76,20 +73,20 @@ <% @report_data["reports"].values.each do | report | %> <div class='report-listing'> <div class="alert alert-info"> - <a class="accordion-toggle" data-toggle="collapse" data-parent="#reportListing" href="#reportListing_<%= report["uri_suffix"] %>"> - <%= I18n.t("reports.#{report["model"]}", :default => report["model"]) %> + <a class="accordion-toggle" data-toggle="collapse" data-parent="#reportListing" href="#reportListing_<%= report["code"] %>"> + <%= I18n.t("reports.#{report["code"]}.title") %> </a> - <button class="pull-right btn btn-default hide selected-message" disabled>Selected</span> - <button class="pull-right btn btn-default select-record" data-report="<%= report['uri_suffix'] %>">Select</button> - <a class="pull-right btn btn-default accordion-toggle" data-toggle="collapse" data-parent="#reportListing" href="#reportListing_<%= report["uri_suffix"] %>" > + <button class="pull-right btn btn-default hide selected-message" disabled>Selected</button> + <button class="pull-right btn btn-default select-record" data-report="<%= report['code'] %>">Select</button> + <a class="pull-right btn btn-default accordion-toggle" data-toggle="collapse" data-parent="#reportListing" href="#reportListing_<%= report["code"] %>" > <%= I18n.t("job.show_description") %> </a> </div> - <div id="reportListing_<%= report["uri_suffix"] %>" class="accordion-body collapse"> + <div id="reportListing_<%= report["code"] %>" class="accordion-body collapse"> <div class="accordion-inner"> - <p><%= report['description'] %></p> + <p><%= I18n.t("reports.#{report["code"]}.description", :default => report["code"]) %></p> <hr/> - <input id='report_type_' name='report_job[report_type]' type='hidden' value='<%= report['uri_suffix'] %>' /> + <input id='report_type_' name='report_job[report_type]' type='hidden' value='<%= report['code'] %>' /> <% report_params = report["params"].reject{|p| ["format", "repo_id"].include?(p[0])} %> <% if report_params.length > 0 %> @@ -132,14 +129,23 @@ </div> <% end %> + <%# Now create a template for all job types not handled above - eg from plugins %> + <% job_types.keys.each do |type| %> + <% next if ['find_and_replace_job', 'print_to_pdf_job', 'import_job', 'report_job'].include?(type) %> + <% define_template(type, jsonmodel_definition(type.intern)) do |form| %> + + <%= render_aspace_partial :partial => "#{type}/form", :locals => {:object => @job, :form => form} %> + <input name="<%= type %>[jsonmodel_type]" type="hidden" value="<%= type %>"> + <% end %> + <% end %> <%= form_for @job, :as => "job", :url => {:action => :create}, :html => {:id => "jobfileupload", :class => 'form-horizontal aspace-record-form', :multipart => true} do |f| %> <%= form_context :job, @job do |form| %> <div class="record-pane"> <%= link_to_help :topic => "job" %> - <h2><%= I18n.t("job._frontend.actions.new") %></h2> + <h2><%= I18n.t("job._frontend.actions.new") %> — <%= I18n.t("job.types.#{params[:job_type]}", :default => params[:job_type]) %> </h2> <%= render_aspace_partial :partial => "shared/form_messages", :locals => {:object => @job, :form => form} %> diff --git a/frontend/app/views/jobs/_listing.html.erb b/frontend/app/views/jobs/_listing.html.erb index 9a656d1262..0020aa457e 100644 --- a/frontend/app/views/jobs/_listing.html.erb +++ b/frontend/app/views/jobs/_listing.html.erb @@ -19,7 +19,7 @@ <% end %> </span> </td> - <td><%= I18n.t("job.job_type_#{job["job_type"]}", :default => job["job_type"]) %></td> + <td><%= I18n.t("job.types.#{job["job_type"]}", :default => job["job_type"]) %></td> <td> <% if job['job_type'] == 'import_job' %> <%= job['job']["filenames"].join("<br/>").html_safe %> diff --git a/frontend/app/views/jobs/_show_templates.html.erb b/frontend/app/views/jobs/_show_templates.html.erb index 2686e274a4..d244867d0e 100644 --- a/frontend/app/views/jobs/_show_templates.html.erb +++ b/frontend/app/views/jobs/_show_templates.html.erb @@ -109,6 +109,18 @@ <% end %> +<%# Now create a template for all job types not handled above - eg from plugins %> +<% job_types.keys.each do |type| %> + <% next if ['find_and_replace_job', 'print_to_pdf_job', 'import_job', 'container_conversion_job', 'report_job'].include?(type) %> + <% define_template(type, jsonmodel_definition(type.intern)) do |form, job| %> + + <%= render_aspace_partial :partial => "#{type}/show", :locals => {:job => job, :form => form} %> + + <% end %> +<% end %> + + + <div id="template_job_running_notice"><!-- diff --git a/frontend/app/views/jobs/_toolbar.html.erb b/frontend/app/views/jobs/_toolbar.html.erb index d639db3e04..90bd3a86dd 100644 --- a/frontend/app/views/jobs/_toolbar.html.erb +++ b/frontend/app/views/jobs/_toolbar.html.erb @@ -1,12 +1,14 @@ -<% if user_can?('cancel_importer_job') && ["queued", "running"].include?(@job.status) && "true" == AppConfig[:jobs_cancelable].to_s %> - <div class="record-toolbar"> - <div class="btn-group btn-toolbar pull-right"> - <div class="btn btn-inline-form"> - <%= button_confirm_action I18n.t("job._frontend.actions.cancel"), url_for(:controller => :jobs, :action => :cancel, :id => @job.id), { - :class => "btn btn-sm btn-danger" - } %> +<% if user_can?('cancel_job') && ["queued", "running"].include?(@job.status) && "true" == AppConfig[:jobs_cancelable].to_s %> + <% if job_types[@job.job_type]['cancel_permissions'].reject{|perm| user_can?(perm)}.empty? %> + <div class="record-toolbar"> + <div class="btn-group btn-toolbar pull-right"> + <div class="btn btn-inline-form"> + <%= button_confirm_action I18n.t("job._frontend.actions.cancel"), url_for(:controller => :jobs, :action => :cancel, :id => @job.id), { + :class => "btn btn-sm btn-danger" + } %> + </div> </div> + <div class="clearfix"></div> </div> - <div class="clearfix"></div> - </div> + <% end %> <% end %> diff --git a/frontend/app/views/jobs/index.html.erb b/frontend/app/views/jobs/index.html.erb index 64ea8f50ee..733b923a11 100644 --- a/frontend/app/views/jobs/index.html.erb +++ b/frontend/app/views/jobs/index.html.erb @@ -4,7 +4,23 @@ <div class="col-md-12"> <div class="record-toolbar"> <div class="pull-right"> - <%= link_to I18n.t("job._frontend.actions.create"), {:controller => :jobs, :action => :new}, :class => "btn btn-sm btn-default" %> + <div class="btn-group"> + <% if user_can?('create_job') %> + <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="javascript:void(0);"> + <%= I18n.t("job._frontend.actions.create") %> + <span class="caret"></span> + </a> + <ul class="dropdown-menu open-aligned-right"> + <% job_types.each do |type, perms| %> + <% if perms['create_permissions'].reject{|perm| user_can?(perm)}.empty? %> + <li> + <%= link_to I18n.t("job.types.#{type}"), :controller => :jobs, :action => :new, :job_type => type %> + </li> + <% end %> + <% end %> + </ul> + <% end %> + </div> </div> <div class="clearfix"></div> </div> diff --git a/frontend/app/views/jobs/report_partials/_locationlist.html.erb b/frontend/app/views/jobs/report_partials/_locationlist.html.erb index a14b5134f8..cd92507556 100644 --- a/frontend/app/views/jobs/report_partials/_locationlist.html.erb +++ b/frontend/app/views/jobs/report_partials/_locationlist.html.erb @@ -6,16 +6,16 @@ building_list = JSONModel::HTTP.get_json("/space_calculator/buildings") %> <select id="location_report_type" name="job[job_params][location_report_type]" class="form-control"> - <option value="repository"><%= I18n.t('reports.location_holdings.repository_report_type') %></option> - <option value="building"><%= I18n.t('reports.location_holdings.building_report_type') %></option> - <option value="single_location"><%= I18n.t('reports.location_holdings.single_location_report_type') %></option> - <option value="location_range"><%= I18n.t('reports.location_holdings.location_range_report_type') %></option> + <option value="repository"><%= I18n.t('reports.location_holdings_report.repository_report_type') %></option> + <option value="building"><%= I18n.t('reports.location_holdings_report.building_report_type') %></option> + <option value="single_location"><%= I18n.t('reports.location_holdings_report.single_location_report_type') %></option> + <option value="location_range"><%= I18n.t('reports.location_holdings_report.location_range_report_type') %></option> </select> <div class="report_type repository"> <hr> <div class="form-group required"> - <label class="control-label col-sm-2"><%= I18n.t('reports.location_holdings.repository_report_type') %></label> + <label class="control-label col-sm-2"><%= I18n.t('reports.location_holdings_report.repository_report_type') %></label> <div class="controls col-sm-8"> <%= select_tag "job[job_params][repository_uri]", options_for_select([""].concat(repository_list)), :class => "form-control" %> </div> @@ -26,7 +26,7 @@ building_list = JSONModel::HTTP.get_json("/space_calculator/buildings") <div class="report_type building"> <hr> <div class="form-group required"> - <label class="control-label col-sm-2"><%= I18n.t('reports.location_holdings.building_report_type') %></label> + <label class="control-label col-sm-2"><%= I18n.t('reports.location_holdings_report.building_report_type') %></label> <div class="controls col-sm-8"> <%= select_tag "job[job_params][building]", options_for_select([""].concat(building_list.keys)), :class => "form-control" %> </div> @@ -36,7 +36,7 @@ building_list = JSONModel::HTTP.get_json("/space_calculator/buildings") <div class="report_type single_location location_range"> <div id="report_location_start" - data-range-label="<%= I18n.t('reports.location_holdings.start_range') %>" + data-range-label="<%= I18n.t('reports.location_holdings_report.start_range') %>" data-singular-label="<%= I18n.t('location._singular') %>"> <% form.push("location_start") do |form| %> <%= render_aspace_partial :partial => "locations/linker", :locals => { :form => form, :hide_create => true } %> @@ -44,7 +44,7 @@ building_list = JSONModel::HTTP.get_json("/space_calculator/buildings") </div> <div id="report_location_end" - data-range-label="<%= I18n.t('reports.location_holdings.end_range') %>" + data-range-label="<%= I18n.t('reports.location_holdings_report.end_range') %>" style="display: none"> <% form.push("location_end") do |form| %> <%= render_aspace_partial :partial => "locations/linker", :locals => { :form => form, :hide_create => true } %> diff --git a/frontend/app/views/layouts/application.html.erb b/frontend/app/views/layouts/application.html.erb index c10255ec91..39e29461ad 100644 --- a/frontend/app/views/layouts/application.html.erb +++ b/frontend/app/views/layouts/application.html.erb @@ -3,7 +3,7 @@ <head> <title><%= I18n.t("navbar.brand") %> | <% if @title %><%= @title %><% else %><%= controller.class.name %> >> <%= controller.action_name %><% end %> - <%= favicon_link_tag %> + <%= include_theme_css %> <%= csrf_meta_tags %> @@ -35,7 +35,7 @@ <% ASUtils.find_local_directories('frontend/views/layout_head.html.erb').each do |layout| %> - <% if File.exists?(layout) %> + <% if File.exist?(layout) %> <%= render :file => layout %> diff --git a/frontend/app/views/notes/_template.html.erb b/frontend/app/views/notes/_template.html.erb index b2e3d80a46..e73f9be4fb 100644 --- a/frontend/app/views/notes/_template.html.erb +++ b/frontend/app/views/notes/_template.html.erb @@ -218,9 +218,9 @@ <% else %>
- -
- <% multipart_subnotes.sort_by {|value, hash| hash[:i18n]}.each do |value, hash| %> @@ -291,9 +291,9 @@ <% else %>
- -
- <% bioghist_subnotes.sort_by {|value, hash| hash[:i18n]}.each do |value, hash| %> diff --git a/frontend/app/views/reports/index.html.erb b/frontend/app/views/reports/index.html.erb index f7614dde5e..5cff714d8a 100644 --- a/frontend/app/views/reports/index.html.erb +++ b/frontend/app/views/reports/index.html.erb @@ -9,15 +9,15 @@ <% @report_data["reports"].values.each do | report | %>
-
" class="accordion-body collapse <% if @report_errors && @report && report['model'] === @report['model'] %>in<% end %>"> +
" class="accordion-body collapse <% if @report_errors && @report && report['model'] === @report['model'] %>in<% end %>">

<%= report['description'] %>


- <% if @report_errors && @report["uri_suffix"] === report["uri_suffix"] %> + <% if @report_errors && @report["code"] === report["code"] %>
<% @report_errors['error'].each do | field, msgs |%>
<%= I18n.t("reports.parameters.#{field}", :default => field) %> - <%= msgs.join(", ") %>
@@ -25,7 +25,7 @@
<% end %> <%= form_tag({:action => :download}, {:class => 'form-horizontal form-report'}) do |f| %> - <%= hidden_field_tag "report_key", report["uri_suffix"] %> + <%= hidden_field_tag "report_key", report["code"] %> <%= form_context "report_params", @report_params do |form| %> <% report_params = report["params"].reject{|p| ["format", "repo_id"].include?(p[0])} @@ -35,7 +35,7 @@ <% report_params.each do | param | %> <%= @report_params.inspect %> -
error<% end %>"> +
error<% end %>">
<%= @report_params[param[0]].inspect %> diff --git a/frontend/app/views/repositories/_form_container.html.erb b/frontend/app/views/repositories/_form_container.html.erb index 55e0a36e37..c92923893e 100644 --- a/frontend/app/views/repositories/_form_container.html.erb +++ b/frontend/app/views/repositories/_form_container.html.erb @@ -5,6 +5,7 @@
<%= form.label_and_textfield "repo_code", :field_opts => {:size => 10} %> <%= form.label_and_textarea "name" %> + <%= form.label_and_boolean "publish", {}, user_prefs["publish"] %>
<%= form.label_and_textfield "org_code", :field_opts => {:size => 10} %> <%= form.label_and_textfield "parent_institution_name" %> diff --git a/frontend/app/views/repositories/index.html.erb b/frontend/app/views/repositories/index.html.erb index b58d9a10f5..5917f1d80a 100644 --- a/frontend/app/views/repositories/index.html.erb +++ b/frontend/app/views/repositories/index.html.erb @@ -1,5 +1,9 @@ <%= setup_context(:title => I18n.t("repository._plural")) %> +<% + add_column(I18n.t("repository.publish"), proc {|repo| I18n.t("boolean.#{repo['publish']}") }) +%> +
- - <% if @refresh_tree_node - node_data = { - 'id' => @resource.id, - 'uri' => @resource.uri, - 'title' => @resource.title, - 'level' => @resource.level==='otherlevel' ? @resource.other_level : I18n.t("enumerations.archival_record_level.#{@resource.level}", :default => @resource.level), - 'jsonmodel_type' => @resource.jsonmodel_type, - 'node_type' => @resource.jsonmodel_type, - 'instance_types' => @resource.instances.map{|instance| I18n.t("enumerations.instance_instance_type.#{instance['instance_type']}", :default => instance['instance_type'])}, - 'containers' => @resource.to_hash["instances"].map{|instance| instance["sub_container"]}.compact.map {|sub_container| - properties = {} - if sub_container["top_container"] - top_container = sub_container["top_container"]["_resolved"] - # if _resolved comes from search result, then need to parse JSON - if top_container["json"] - top_container = ASUtils.json_parse(top_container["json"]) - end - properties["type_1"] = "Container" - properties["indicator_1"] = top_container["indicator"] - if top_container["barcode"] - properties["indicator_1"] += " [#{top_container["barcode"]}]" - end - end - properties["type_2"] = I18n.t("enumerations.container_type.#{sub_container["type_2"]}", :default => sub_container["type_2"]) - properties["indicator_2"] =sub_container["indicator_2"] - properties["type_3"] = I18n.t("enumerations.container_type.#{sub_container["type_3"]}", :default => sub_container["type_3"]) - properties["indicator_3"] =sub_container["indicator_3"] - properties - }, - 'replace_new_node' => false - } - %> - - <% end %> <% end %> <% end %> diff --git a/frontend/app/views/resources/_form_container.html.erb b/frontend/app/views/resources/_form_container.html.erb index 7eb7403a35..a8c3766c3b 100644 --- a/frontend/app/views/resources/_form_container.html.erb +++ b/frontend/app/views/resources/_form_container.html.erb @@ -8,6 +8,7 @@ <%= form.hidden_input "related_accession" %> <%= hidden_field_tag "id", @resource.id %> +<%= hidden_field_tag "uri", @resource.uri %> <% define_template("resource", jsonmodel_definition(:resource)) do |form| %> diff --git a/frontend/app/views/resources/edit.html.erb b/frontend/app/views/resources/edit.html.erb index 90d425a7a9..8ca7c76c91 100644 --- a/frontend/app/views/resources/edit.html.erb +++ b/frontend/app/views/resources/edit.html.erb @@ -1,9 +1,3 @@ <%= setup_context :object => @resource %> -
-
- <%= render_aspace_partial :partial => "shared/tree", :locals => {:root_record => @resource} %> -
-
- -
+<%= render_aspace_partial :partial => "shared/largetree", :locals => {:root_record => @resource, :read_only => false} %> diff --git a/frontend/app/views/resources/show.html.erb b/frontend/app/views/resources/show.html.erb index a89d0b36bb..d66ccf37c9 100644 --- a/frontend/app/views/resources/show.html.erb +++ b/frontend/app/views/resources/show.html.erb @@ -1,11 +1,4 @@ <%= setup_context :object => @resource %> <%= render_aspace_partial :partial => "transfer/transfer_failures", :locals => { :transfer_errors => @transfer_errors } %> - -
-
- <%= render_aspace_partial :partial => "shared/tree", :locals => {:root_record => @resource, :read_only => true} %> -
-
- -
+<%= render_aspace_partial :partial => "shared/largetree", :locals => {:root_record => @resource, :read_only => true} %> \ No newline at end of file diff --git a/frontend/app/views/session/select_user.html.erb b/frontend/app/views/session/select_user.html.erb index 4799b02ac7..a54b11e62d 100644 --- a/frontend/app/views/session/select_user.html.erb +++ b/frontend/app/views/session/select_user.html.erb @@ -8,7 +8,7 @@ <%= render_aspace_partial :partial => "shared/flash_messages" %> - <%= form_for @become_user, :as => "become_user", :url => {:action => :become_user}, :html => {:class => 'form-horizontal'} do |f| %> + <%= form_tag({:action => :become_user}, {:class => 'form-horizontal', :id => 'new_become_user'}) do |f| %> "/> <% end %> diff --git a/frontend/app/views/shared/_advanced_search.html.erb b/frontend/app/views/shared/_advanced_search.html.erb index 6ed21cf0a9..67c96913c9 100644 --- a/frontend/app/views/shared/_advanced_search.html.erb +++ b/frontend/app/views/shared/_advanced_search.html.erb @@ -14,6 +14,7 @@ options_for_enums = Hash[values_for[:enum].map {|op| options = JSONModel.enum_values(op.field).map {|value| [I18n.t("enumerations.#{op.field}.#{value}"), value]} + options << [I18n.t("advanced_search.enum_field.empty"), ''] [op.field, options] }] @@ -34,7 +35,7 @@ var container = $(".enum-select-container" + index); container.empty(); - var value_select = $(''); $(advanced_search_enum_values[selected_enum]).each(function (idx, op) { var label = op[0]; @@ -45,6 +46,16 @@ if (value === existing_selection) { option.attr('selected', 'selected'); + } else if(value == '' && existing_selection == null) { + option.attr('selected', 'selected'); + } + + // enter a separator before the empty "unassigned" option + if (value == '') { + var separator = $('
- <%= text_field_tag "v${index}", "${query.value}", :id => "v${index}", :class => 'form-control'%> +
+
+ +
+ <%= text_field_tag "v${index}", "${query.value}", :id => "v${index}", :class => 'form-control'%> +
-->
@@ -124,15 +187,15 @@
-
- -
-
- <%= text_field_tag "v${index}", "${query.value}", :id => "v${index}", :class => "date-field form-control", :"data-format" => "yyyy-mm-dd", :"data-date" => Date.today.strftime('%Y-%m-%d') %> +
+
+ +
+ <%= text_field_tag "v${index}", "${query.value}", :id => "v${index}", :class => "date-field form-control", :"data-format" => "yyyy-mm-dd", :"data-date" => Date.today.strftime('%Y-%m-%d') %>
-->
@@ -157,7 +220,7 @@
- - <%= form_tag(url_for(:controller => :search, :action => :advanced_search), :method => :get, :class => "advanced-search") do %> <%= hidden_field_tag "advanced", true %> diff --git a/frontend/app/views/shared/_header_repository.html.erb b/frontend/app/views/shared/_header_repository.html.erb index efe5e89ea4..8d92c0ac31 100644 --- a/frontend/app/views/shared/_header_repository.html.erb +++ b/frontend/app/views/shared/_header_repository.html.erb @@ -82,8 +82,21 @@ <% if user_can?('update_classification_record') %>
  • <%= link_to I18n.t("classification._singular"), :controller => :classifications, :action => :new %>
  • <% end %> -
  • -
  • <%= link_to I18n.t("job._plural"), :controller => :jobs, :action => :new %>
  • + + <% if user_can?('create_job') %> +
  • + + + <% end %> <% end %> @@ -102,11 +115,14 @@ "); - this.element.attr('aria-activedescendant','j' + this._id + '_loading'); - this._data.core.li_height = this.get_container_ul().children("li").first().height() || 24; - /** - * triggered after the loading text is shown and before loading starts - * @event - * @name loading.jstree - */ - this.trigger("loading"); - this.load_node($.jstree.root); - }, - /** - * destroy an instance - * @name destroy() - * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact - */ - destroy : function (keep_html) { - if(this._wrk) { - try { - window.URL.revokeObjectURL(this._wrk); - this._wrk = null; - } - catch (ignore) { } - } - if(!keep_html) { this.element.empty(); } - this.teardown(); - }, - /** - * part of the destroying of an instance. Used internally. - * @private - * @name teardown() - */ - teardown : function () { - this.unbind(); - this.element - .removeClass('jstree') - .removeData('jstree') - .find("[class^='jstree']") - .addBack() - .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); - this.element = null; - }, - /** - * bind all events. Used internally. - * @private - * @name bind() - */ - bind : function () { - var word = '', - tout = null, - was_click = 0; - this.element - .on("dblclick.jstree", function (e) { - if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } - if(document.selection && document.selection.empty) { - document.selection.empty(); - } - else { - if(window.getSelection) { - var sel = window.getSelection(); - try { - sel.removeAllRanges(); - sel.collapse(); - } catch (ignore) { } - } - } - }) - .on("mousedown.jstree", $.proxy(function (e) { - if(e.target === this.element[0]) { - e.preventDefault(); // prevent losing focus when clicking scroll arrows (FF, Chrome) - was_click = +(new Date()); // ie does not allow to prevent losing focus - } - }, this)) - .on("mousedown.jstree", ".jstree-ocl", function (e) { - e.preventDefault(); // prevent any node inside from losing focus when clicking the open/close icon - }) - .on("click.jstree", ".jstree-ocl", $.proxy(function (e) { - this.toggle_node(e.target); - }, this)) - .on("dblclick.jstree", ".jstree-anchor", $.proxy(function (e) { - if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } - if(this.settings.core.dblclick_toggle) { - this.toggle_node(e.target); - } - }, this)) - .on("click.jstree", ".jstree-anchor", $.proxy(function (e) { - e.preventDefault(); - if(e.currentTarget !== document.activeElement) { $(e.currentTarget).focus(); } - this.activate_node(e.currentTarget, e); - }, this)) - .on('keydown.jstree', '.jstree-anchor', $.proxy(function (e) { - if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } - if(e.which !== 32 && e.which !== 13 && (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) { return true; } - var o = null; - if(this._data.core.rtl) { - if(e.which === 37) { e.which = 39; } - else if(e.which === 39) { e.which = 37; } - } - switch(e.which) { - case 32: // aria defines space only with Ctrl - if(e.ctrlKey) { - e.type = "click"; - $(e.currentTarget).trigger(e); - } - break; - case 13: // enter - e.type = "click"; - $(e.currentTarget).trigger(e); - break; - case 37: // right - e.preventDefault(); - if(this.is_open(e.currentTarget)) { - this.close_node(e.currentTarget); - } - else { - o = this.get_parent(e.currentTarget); - if(o && o.id !== $.jstree.root) { this.get_node(o, true).children('.jstree-anchor').focus(); } - } - break; - case 38: // up - e.preventDefault(); - o = this.get_prev_dom(e.currentTarget); - if(o && o.length) { o.children('.jstree-anchor').focus(); } - break; - case 39: // left - e.preventDefault(); - if(this.is_closed(e.currentTarget)) { - this.open_node(e.currentTarget, function (o) { this.get_node(o, true).children('.jstree-anchor').focus(); }); - } - else if (this.is_open(e.currentTarget)) { - o = this.get_node(e.currentTarget, true).children('.jstree-children')[0]; - if(o) { $(this._firstChild(o)).children('.jstree-anchor').focus(); } - } - break; - case 40: // down - e.preventDefault(); - o = this.get_next_dom(e.currentTarget); - if(o && o.length) { o.children('.jstree-anchor').focus(); } - break; - case 106: // aria defines * on numpad as open_all - not very common - this.open_all(); - break; - case 36: // home - e.preventDefault(); - o = this._firstChild(this.get_container_ul()[0]); - if(o) { $(o).children('.jstree-anchor').filter(':visible').focus(); } - break; - case 35: // end - e.preventDefault(); - this.element.find('.jstree-anchor').filter(':visible').last().focus(); - break; - /*! - // delete - case 46: - e.preventDefault(); - o = this.get_node(e.currentTarget); - if(o && o.id && o.id !== $.jstree.root) { - o = this.is_selected(o) ? this.get_selected() : o; - this.delete_node(o); - } - break; - // f2 - case 113: - e.preventDefault(); - o = this.get_node(e.currentTarget); - if(o && o.id && o.id !== $.jstree.root) { - // this.edit(o); - } - break; - default: - // console.log(e.which); - break; - */ - } - }, this)) - .on("load_node.jstree", $.proxy(function (e, data) { - if(data.status) { - if(data.node.id === $.jstree.root && !this._data.core.loaded) { - this._data.core.loaded = true; - if(this._firstChild(this.get_container_ul()[0])) { - this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id); - } - /** - * triggered after the root node is loaded for the first time - * @event - * @name loaded.jstree - */ - this.trigger("loaded"); - } - if(!this._data.core.ready) { - setTimeout($.proxy(function() { - if(this.element && !this.get_container_ul().find('.jstree-loading').length) { - this._data.core.ready = true; - if(this._data.core.selected.length) { - if(this.settings.core.expand_selected_onload) { - var tmp = [], i, j; - for(i = 0, j = this._data.core.selected.length; i < j; i++) { - tmp = tmp.concat(this._model.data[this._data.core.selected[i]].parents); - } - tmp = $.vakata.array_unique(tmp); - for(i = 0, j = tmp.length; i < j; i++) { - this.open_node(tmp[i], false, 0); - } - } - this.trigger('changed', { 'action' : 'ready', 'selected' : this._data.core.selected }); - } - /** - * triggered after all nodes are finished loading - * @event - * @name ready.jstree - */ - this.trigger("ready"); - } - }, this), 0); - } - } - }, this)) - // quick searching when the tree is focused - .on('keypress.jstree', $.proxy(function (e) { - if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } - if(tout) { clearTimeout(tout); } - tout = setTimeout(function () { - word = ''; - }, 500); - - var chr = String.fromCharCode(e.which).toLowerCase(), - col = this.element.find('.jstree-anchor').filter(':visible'), - ind = col.index(document.activeElement) || 0, - end = false; - word += chr; - - // match for whole word from current node down (including the current node) - if(word.length > 1) { - col.slice(ind).each($.proxy(function (i, v) { - if($(v).text().toLowerCase().indexOf(word) === 0) { - $(v).focus(); - end = true; - return false; - } - }, this)); - if(end) { return; } - - // match for whole word from the beginning of the tree - col.slice(0, ind).each($.proxy(function (i, v) { - if($(v).text().toLowerCase().indexOf(word) === 0) { - $(v).focus(); - end = true; - return false; - } - }, this)); - if(end) { return; } - } - // list nodes that start with that letter (only if word consists of a single char) - if(new RegExp('^' + chr.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '+$').test(word)) { - // search for the next node starting with that letter - col.slice(ind + 1).each($.proxy(function (i, v) { - if($(v).text().toLowerCase().charAt(0) === chr) { - $(v).focus(); - end = true; - return false; - } - }, this)); - if(end) { return; } - - // search from the beginning - col.slice(0, ind + 1).each($.proxy(function (i, v) { - if($(v).text().toLowerCase().charAt(0) === chr) { - $(v).focus(); - end = true; - return false; - } - }, this)); - if(end) { return; } - } - }, this)) - // THEME RELATED - .on("init.jstree", $.proxy(function () { - var s = this.settings.core.themes; - this._data.core.themes.dots = s.dots; - this._data.core.themes.stripes = s.stripes; - this._data.core.themes.icons = s.icons; - this.set_theme(s.name || "default", s.url); - this.set_theme_variant(s.variant); - }, this)) - .on("loading.jstree", $.proxy(function () { - this[ this._data.core.themes.dots ? "show_dots" : "hide_dots" ](); - this[ this._data.core.themes.icons ? "show_icons" : "hide_icons" ](); - this[ this._data.core.themes.stripes ? "show_stripes" : "hide_stripes" ](); - }, this)) - .on('blur.jstree', '.jstree-anchor', $.proxy(function (e) { - this._data.core.focused = null; - $(e.currentTarget).filter('.jstree-hovered').mouseleave(); - this.element.attr('tabindex', '0'); - }, this)) - .on('focus.jstree', '.jstree-anchor', $.proxy(function (e) { - var tmp = this.get_node(e.currentTarget); - if(tmp && tmp.id) { - this._data.core.focused = tmp.id; - } - this.element.find('.jstree-hovered').not(e.currentTarget).mouseleave(); - $(e.currentTarget).mouseenter(); - this.element.attr('tabindex', '-1'); - }, this)) - .on('focus.jstree', $.proxy(function () { - if(+(new Date()) - was_click > 500 && !this._data.core.focused) { - was_click = 0; - var act = this.get_node(this.element.attr('aria-activedescendant'), true); - if(act) { - act.find('> .jstree-anchor').focus(); - } - } - }, this)) - .on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e) { - this.hover_node(e.currentTarget); - }, this)) - .on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e) { - this.dehover_node(e.currentTarget); - }, this)); - }, - /** - * part of the destroying of an instance. Used internally. - * @private - * @name unbind() - */ - unbind : function () { - this.element.off('.jstree'); - $(document).off('.jstree-' + this._id); - }, - /** - * trigger an event. Used internally. - * @private - * @name trigger(ev [, data]) - * @param {String} ev the name of the event to trigger - * @param {Object} data additional data to pass with the event - */ - trigger : function (ev, data) { - if(!data) { - data = {}; - } - data.instance = this; - this.element.triggerHandler(ev.replace('.jstree','') + '.jstree', data); - }, - /** - * returns the jQuery extended instance container - * @name get_container() - * @return {jQuery} - */ - get_container : function () { - return this.element; - }, - /** - * returns the jQuery extended main UL node inside the instance container. Used internally. - * @private - * @name get_container_ul() - * @return {jQuery} - */ - get_container_ul : function () { - return this.element.children(".jstree-children").first(); - }, - /** - * gets string replacements (localization). Used internally. - * @private - * @name get_string(key) - * @param {String} key - * @return {String} - */ - get_string : function (key) { - var a = this.settings.core.strings; - if($.isFunction(a)) { return a.call(this, key); } - if(a && a[key]) { return a[key]; } - return key; - }, - /** - * gets the first child of a DOM node. Used internally. - * @private - * @name _firstChild(dom) - * @param {DOMElement} dom - * @return {DOMElement} - */ - _firstChild : function (dom) { - dom = dom ? dom.firstChild : null; - while(dom !== null && dom.nodeType !== 1) { - dom = dom.nextSibling; - } - return dom; - }, - /** - * gets the next sibling of a DOM node. Used internally. - * @private - * @name _nextSibling(dom) - * @param {DOMElement} dom - * @return {DOMElement} - */ - _nextSibling : function (dom) { - dom = dom ? dom.nextSibling : null; - while(dom !== null && dom.nodeType !== 1) { - dom = dom.nextSibling; - } - return dom; - }, - /** - * gets the previous sibling of a DOM node. Used internally. - * @private - * @name _previousSibling(dom) - * @param {DOMElement} dom - * @return {DOMElement} - */ - _previousSibling : function (dom) { - dom = dom ? dom.previousSibling : null; - while(dom !== null && dom.nodeType !== 1) { - dom = dom.previousSibling; - } - return dom; - }, - /** - * get the JSON representation of a node (or the actual jQuery extended DOM node) by using any input (child DOM element, ID string, selector, etc) - * @name get_node(obj [, as_dom]) - * @param {mixed} obj - * @param {Boolean} as_dom - * @return {Object|jQuery} - */ - get_node : function (obj, as_dom) { - if(obj && obj.id) { - obj = obj.id; - } - var dom; - try { - if(this._model.data[obj]) { - obj = this._model.data[obj]; - } - else if(typeof obj === "string" && this._model.data[obj.replace(/^#/, '')]) { - obj = this._model.data[obj.replace(/^#/, '')]; - } - else if(typeof obj === "string" && (dom = $('#' + obj.replace($.jstree.idregex,'\\$&'), this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) { - obj = this._model.data[dom.closest('.jstree-node').attr('id')]; - } - else if((dom = $(obj, this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) { - obj = this._model.data[dom.closest('.jstree-node').attr('id')]; - } - else if((dom = $(obj, this.element)).length && dom.hasClass('jstree')) { - obj = this._model.data[$.jstree.root]; - } - else { - return false; - } - - if(as_dom) { - obj = obj.id === $.jstree.root ? this.element : $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element); - } - return obj; - } catch (ex) { return false; } - }, - /** - * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array) - * @name get_path(obj [, glue, ids]) - * @param {mixed} obj the node - * @param {String} glue if you want the path as a string - pass the glue here (for example '/'), if a falsy value is supplied here, an array is returned - * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used - * @return {mixed} - */ - get_path : function (obj, glue, ids) { - obj = obj.parents ? obj : this.get_node(obj); - if(!obj || obj.id === $.jstree.root || !obj.parents) { - return false; - } - var i, j, p = []; - p.push(ids ? obj.id : obj.text); - for(i = 0, j = obj.parents.length; i < j; i++) { - p.push(ids ? obj.parents[i] : this.get_text(obj.parents[i])); - } - p = p.reverse().slice(1); - return glue ? p.join(glue) : p; - }, - /** - * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned. - * @name get_next_dom(obj [, strict]) - * @param {mixed} obj - * @param {Boolean} strict - * @return {jQuery} - */ - get_next_dom : function (obj, strict) { - var tmp; - obj = this.get_node(obj, true); - if(obj[0] === this.element[0]) { - tmp = this._firstChild(this.get_container_ul()[0]); - while (tmp && tmp.offsetHeight === 0) { - tmp = this._nextSibling(tmp); - } - return tmp ? $(tmp) : false; - } - if(!obj || !obj.length) { - return false; - } - if(strict) { - tmp = obj[0]; - do { - tmp = this._nextSibling(tmp); - } while (tmp && tmp.offsetHeight === 0); - return tmp ? $(tmp) : false; - } - if(obj.hasClass("jstree-open")) { - tmp = this._firstChild(obj.children('.jstree-children')[0]); - while (tmp && tmp.offsetHeight === 0) { - tmp = this._nextSibling(tmp); - } - if(tmp !== null) { - return $(tmp); - } - } - tmp = obj[0]; - do { - tmp = this._nextSibling(tmp); - } while (tmp && tmp.offsetHeight === 0); - if(tmp !== null) { - return $(tmp); - } - return obj.parentsUntil(".jstree",".jstree-node").nextAll(".jstree-node:visible").first(); - }, - /** - * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned. - * @name get_prev_dom(obj [, strict]) - * @param {mixed} obj - * @param {Boolean} strict - * @return {jQuery} - */ - get_prev_dom : function (obj, strict) { - var tmp; - obj = this.get_node(obj, true); - if(obj[0] === this.element[0]) { - tmp = this.get_container_ul()[0].lastChild; - while (tmp && tmp.offsetHeight === 0) { - tmp = this._previousSibling(tmp); - } - return tmp ? $(tmp) : false; - } - if(!obj || !obj.length) { - return false; - } - if(strict) { - tmp = obj[0]; - do { - tmp = this._previousSibling(tmp); - } while (tmp && tmp.offsetHeight === 0); - return tmp ? $(tmp) : false; - } - tmp = obj[0]; - do { - tmp = this._previousSibling(tmp); - } while (tmp && tmp.offsetHeight === 0); - if(tmp !== null) { - obj = $(tmp); - while(obj.hasClass("jstree-open")) { - obj = obj.children(".jstree-children").first().children(".jstree-node:visible:last"); - } - return obj; - } - tmp = obj[0].parentNode.parentNode; - return tmp && tmp.className && tmp.className.indexOf('jstree-node') !== -1 ? $(tmp) : false; - }, - /** - * get the parent ID of a node - * @name get_parent(obj) - * @param {mixed} obj - * @return {String} - */ - get_parent : function (obj) { - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - return obj.parent; - }, - /** - * get a jQuery collection of all the children of a node (node must be rendered) - * @name get_children_dom(obj) - * @param {mixed} obj - * @return {jQuery} - */ - get_children_dom : function (obj) { - obj = this.get_node(obj, true); - if(obj[0] === this.element[0]) { - return this.get_container_ul().children(".jstree-node"); - } - if(!obj || !obj.length) { - return false; - } - return obj.children(".jstree-children").children(".jstree-node"); - }, - /** - * checks if a node has children - * @name is_parent(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_parent : function (obj) { - obj = this.get_node(obj); - return obj && (obj.state.loaded === false || obj.children.length > 0); - }, - /** - * checks if a node is loaded (its children are available) - * @name is_loaded(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_loaded : function (obj) { - obj = this.get_node(obj); - return obj && obj.state.loaded; - }, - /** - * check if a node is currently loading (fetching children) - * @name is_loading(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_loading : function (obj) { - obj = this.get_node(obj); - return obj && obj.state && obj.state.loading; - }, - /** - * check if a node is opened - * @name is_open(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_open : function (obj) { - obj = this.get_node(obj); - return obj && obj.state.opened; - }, - /** - * check if a node is in a closed state - * @name is_closed(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_closed : function (obj) { - obj = this.get_node(obj); - return obj && this.is_parent(obj) && !obj.state.opened; - }, - /** - * check if a node has no children - * @name is_leaf(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_leaf : function (obj) { - return !this.is_parent(obj); - }, - /** - * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array. - * @name load_node(obj [, callback]) - * @param {mixed} obj - * @param {function} callback a function to be executed once loading is complete, the function is executed in the instance's scope and receives two arguments - the node and a boolean status - * @return {Boolean} - * @trigger load_node.jstree - */ - load_node : function (obj, callback) { - var k, l, i, j, c; - if($.isArray(obj)) { - this._load_nodes(obj.slice(), callback); - return true; - } - obj = this.get_node(obj); - if(!obj) { - if(callback) { callback.call(this, obj, false); } - return false; - } - // if(obj.state.loading) { } // the node is already loading - just wait for it to load and invoke callback? but if called implicitly it should be loaded again? - if(obj.state.loaded) { - obj.state.loaded = false; - for(k = 0, l = obj.children_d.length; k < l; k++) { - for(i = 0, j = obj.parents.length; i < j; i++) { - this._model.data[obj.parents[i]].children_d = $.vakata.array_remove_item(this._model.data[obj.parents[i]].children_d, obj.children_d[k]); - } - if(this._model.data[obj.children_d[k]].state.selected) { - c = true; - this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.children_d[k]); - } - delete this._model.data[obj.children_d[k]]; - } - obj.children = []; - obj.children_d = []; - if(c) { - this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected }); - } - } - obj.state.failed = false; - obj.state.loading = true; - this.get_node(obj, true).addClass("jstree-loading").attr('aria-busy',true); - this._load_node(obj, $.proxy(function (status) { - obj = this._model.data[obj.id]; - obj.state.loading = false; - obj.state.loaded = status; - obj.state.failed = !obj.state.loaded; - var dom = this.get_node(obj, true), i = 0, j = 0, m = this._model.data, has_children = false; - for(i = 0, j = obj.children.length; i < j; i++) { - if(m[obj.children[i]] && !m[obj.children[i]].state.hidden) { - has_children = true; - break; - } - } - if(obj.state.loaded && !has_children && dom && dom.length && !dom.hasClass('jstree-leaf')) { - dom.removeClass('jstree-closed jstree-open').addClass('jstree-leaf'); - } - dom.removeClass("jstree-loading").attr('aria-busy',false); - /** - * triggered after a node is loaded - * @event - * @name load_node.jstree - * @param {Object} node the node that was loading - * @param {Boolean} status was the node loaded successfully - */ - this.trigger('load_node', { "node" : obj, "status" : status }); - if(callback) { - callback.call(this, obj, status); - } - }, this)); - return true; - }, - /** - * load an array of nodes (will also load unavailable nodes as soon as the appear in the structure). Used internally. - * @private - * @name _load_nodes(nodes [, callback]) - * @param {array} nodes - * @param {function} callback a function to be executed once loading is complete, the function is executed in the instance's scope and receives one argument - the array passed to _load_nodes - */ - _load_nodes : function (nodes, callback, is_callback) { - var r = true, - c = function () { this._load_nodes(nodes, callback, true); }, - m = this._model.data, i, j, tmp = []; - for(i = 0, j = nodes.length; i < j; i++) { - if(m[nodes[i]] && ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || !is_callback)) { - if(!this.is_loading(nodes[i])) { - this.load_node(nodes[i], c); - } - r = false; - } - } - if(r) { - for(i = 0, j = nodes.length; i < j; i++) { - if(m[nodes[i]] && m[nodes[i]].state.loaded) { - tmp.push(nodes[i]); - } - } - if(callback && !callback.done) { - callback.call(this, tmp); - callback.done = true; - } - } - }, - /** - * loads all unloaded nodes - * @name load_all([obj, callback]) - * @param {mixed} obj the node to load recursively, omit to load all nodes in the tree - * @param {function} callback a function to be executed once loading all the nodes is complete, - * @trigger load_all.jstree - */ - load_all : function (obj, callback) { - if(!obj) { obj = $.jstree.root; } - obj = this.get_node(obj); - if(!obj) { return false; } - var to_load = [], - m = this._model.data, - c = m[obj.id].children_d, - i, j; - if(obj.state && !obj.state.loaded) { - to_load.push(obj.id); - } - for(i = 0, j = c.length; i < j; i++) { - if(m[c[i]] && m[c[i]].state && !m[c[i]].state.loaded) { - to_load.push(c[i]); - } - } - if(to_load.length) { - this._load_nodes(to_load, function () { - this.load_all(obj, callback); - }); - } - else { - /** - * triggered after a load_all call completes - * @event - * @name load_all.jstree - * @param {Object} node the recursively loaded node - */ - if(callback) { callback.call(this, obj); } - this.trigger('load_all', { "node" : obj }); - } - }, - /** - * handles the actual loading of a node. Used only internally. - * @private - * @name _load_node(obj [, callback]) - * @param {mixed} obj - * @param {function} callback a function to be executed once loading is complete, the function is executed in the instance's scope and receives one argument - a boolean status - * @return {Boolean} - */ - _load_node : function (obj, callback) { - var s = this.settings.core.data, t; - // use original HTML - if(!s) { - if(obj.id === $.jstree.root) { - return this._append_html_data(obj, this._data.core.original_container_html.clone(true), function (status) { - callback.call(this, status); - }); - } - else { - return callback.call(this, false); - } - // return callback.call(this, obj.id === $.jstree.root ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false); - } - if($.isFunction(s)) { - return s.call(this, obj, $.proxy(function (d) { - if(d === false) { - callback.call(this, false); - } - this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }) : d, function (status) { - callback.call(this, status); - }); - // return d === false ? callback.call(this, false) : callback.call(this, this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $(d) : d)); - }, this)); - } - if(typeof s === 'object') { - if(s.url) { - s = $.extend(true, {}, s); - if($.isFunction(s.url)) { - s.url = s.url.call(this, obj); - } - if($.isFunction(s.data)) { - s.data = s.data.call(this, obj); - } - return $.ajax(s) - .done($.proxy(function (d,t,x) { - var type = x.getResponseHeader('Content-Type'); - if((type && type.indexOf('json') !== -1) || typeof d === "object") { - return this._append_json_data(obj, d, function (status) { callback.call(this, status); }); - //return callback.call(this, this._append_json_data(obj, d)); - } - if((type && type.indexOf('html') !== -1) || typeof d === "string") { - return this._append_html_data(obj, $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }), function (status) { callback.call(this, status); }); - // return callback.call(this, this._append_html_data(obj, $(d))); - } - this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'core', 'id' : 'core_04', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id, 'xhr' : x }) }; - this.settings.core.error.call(this, this._data.core.last_error); - return callback.call(this, false); - }, this)) - .fail($.proxy(function (f) { - callback.call(this, false); - this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'core', 'id' : 'core_04', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id, 'xhr' : f }) }; - this.settings.core.error.call(this, this._data.core.last_error); - }, this)); - } - t = ($.isArray(s) || $.isPlainObject(s)) ? JSON.parse(JSON.stringify(s)) : s; - if(obj.id === $.jstree.root) { - return this._append_json_data(obj, t, function (status) { - callback.call(this, status); - }); - } - else { - this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) }; - this.settings.core.error.call(this, this._data.core.last_error); - return callback.call(this, false); - } - //return callback.call(this, (obj.id === $.jstree.root ? this._append_json_data(obj, t) : false) ); - } - if(typeof s === 'string') { - if(obj.id === $.jstree.root) { - return this._append_html_data(obj, $($.parseHTML(s)).filter(function () { return this.nodeType !== 3; }), function (status) { - callback.call(this, status); - }); - } - else { - this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) }; - this.settings.core.error.call(this, this._data.core.last_error); - return callback.call(this, false); - } - //return callback.call(this, (obj.id === $.jstree.root ? this._append_html_data(obj, $(s)) : false) ); - } - return callback.call(this, false); - }, - /** - * adds a node to the list of nodes to redraw. Used only internally. - * @private - * @name _node_changed(obj [, callback]) - * @param {mixed} obj - */ - _node_changed : function (obj) { - obj = this.get_node(obj); - if(obj) { - this._model.changed.push(obj.id); - } - }, - /** - * appends HTML content to the tree. Used internally. - * @private - * @name _append_html_data(obj, data) - * @param {mixed} obj the node to append to - * @param {String} data the HTML string to parse and append - * @trigger model.jstree, changed.jstree - */ - _append_html_data : function (dom, data, cb) { - dom = this.get_node(dom); - dom.children = []; - dom.children_d = []; - var dat = data.is('ul') ? data.children() : data, - par = dom.id, - chd = [], - dpc = [], - m = this._model.data, - p = m[par], - s = this._data.core.selected.length, - tmp, i, j; - dat.each($.proxy(function (i, v) { - tmp = this._parse_model_from_html($(v), par, p.parents.concat()); - if(tmp) { - chd.push(tmp); - dpc.push(tmp); - if(m[tmp].children_d.length) { - dpc = dpc.concat(m[tmp].children_d); - } - } - }, this)); - p.children = chd; - p.children_d = dpc; - for(i = 0, j = p.parents.length; i < j; i++) { - m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc); - } - /** - * triggered when new data is inserted to the tree model - * @event - * @name model.jstree - * @param {Array} nodes an array of node IDs - * @param {String} parent the parent ID of the nodes - */ - this.trigger('model', { "nodes" : dpc, 'parent' : par }); - if(par !== $.jstree.root) { - this._node_changed(par); - this.redraw(); - } - else { - this.get_container_ul().children('.jstree-initial-node').remove(); - this.redraw(true); - } - if(this._data.core.selected.length !== s) { - this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected }); - } - cb.call(this, true); - }, - /** - * appends JSON content to the tree. Used internally. - * @private - * @name _append_json_data(obj, data) - * @param {mixed} obj the node to append to - * @param {String} data the JSON object to parse and append - * @param {Boolean} force_processing internal param - do not set - * @trigger model.jstree, changed.jstree - */ - _append_json_data : function (dom, data, cb, force_processing) { - if(this.element === null) { return; } - dom = this.get_node(dom); - dom.children = []; - dom.children_d = []; - // *%$@!!! - if(data.d) { - data = data.d; - if(typeof data === "string") { - data = JSON.parse(data); - } - } - if(!$.isArray(data)) { data = [data]; } - var w = null, - args = { - 'df' : this._model.default_state, - 'dat' : data, - 'par' : dom.id, - 'm' : this._model.data, - 't_id' : this._id, - 't_cnt' : this._cnt, - 'sel' : this._data.core.selected - }, - func = function (data, undefined) { - if(data.data) { data = data.data; } - var dat = data.dat, - par = data.par, - chd = [], - dpc = [], - add = [], - df = data.df, - t_id = data.t_id, - t_cnt = data.t_cnt, - m = data.m, - p = m[par], - sel = data.sel, - tmp, i, j, rslt, - parse_flat = function (d, p, ps) { - if(!ps) { ps = []; } - else { ps = ps.concat(); } - if(p) { ps.unshift(p); } - var tid = d.id.toString(), - i, j, c, e, - tmp = { - id : tid, - text : d.text || '', - icon : d.icon !== undefined ? d.icon : true, - parent : p, - parents : ps, - children : d.children || [], - children_d : d.children_d || [], - data : d.data, - state : { }, - li_attr : { id : false }, - a_attr : { href : '#' }, - original : false - }; - for(i in df) { - if(df.hasOwnProperty(i)) { - tmp.state[i] = df[i]; - } - } - if(d && d.data && d.data.jstree && d.data.jstree.icon) { - tmp.icon = d.data.jstree.icon; - } - if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { - tmp.icon = true; - } - if(d && d.data) { - tmp.data = d.data; - if(d.data.jstree) { - for(i in d.data.jstree) { - if(d.data.jstree.hasOwnProperty(i)) { - tmp.state[i] = d.data.jstree[i]; - } - } - } - } - if(d && typeof d.state === 'object') { - for (i in d.state) { - if(d.state.hasOwnProperty(i)) { - tmp.state[i] = d.state[i]; - } - } - } - if(d && typeof d.li_attr === 'object') { - for (i in d.li_attr) { - if(d.li_attr.hasOwnProperty(i)) { - tmp.li_attr[i] = d.li_attr[i]; - } - } - } - if(!tmp.li_attr.id) { - tmp.li_attr.id = tid; - } - if(d && typeof d.a_attr === 'object') { - for (i in d.a_attr) { - if(d.a_attr.hasOwnProperty(i)) { - tmp.a_attr[i] = d.a_attr[i]; - } - } - } - if(d && d.children && d.children === true) { - tmp.state.loaded = false; - tmp.children = []; - tmp.children_d = []; - } - m[tmp.id] = tmp; - for(i = 0, j = tmp.children.length; i < j; i++) { - c = parse_flat(m[tmp.children[i]], tmp.id, ps); - e = m[c]; - tmp.children_d.push(c); - if(e.children_d.length) { - tmp.children_d = tmp.children_d.concat(e.children_d); - } - } - delete d.data; - delete d.children; - m[tmp.id].original = d; - if(tmp.state.selected) { - add.push(tmp.id); - } - return tmp.id; - }, - parse_nest = function (d, p, ps) { - if(!ps) { ps = []; } - else { ps = ps.concat(); } - if(p) { ps.unshift(p); } - var tid = false, i, j, c, e, tmp; - do { - tid = 'j' + t_id + '_' + (++t_cnt); - } while(m[tid]); - - tmp = { - id : false, - text : typeof d === 'string' ? d : '', - icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true, - parent : p, - parents : ps, - children : [], - children_d : [], - data : null, - state : { }, - li_attr : { id : false }, - a_attr : { href : '#' }, - original : false - }; - for(i in df) { - if(df.hasOwnProperty(i)) { - tmp.state[i] = df[i]; - } - } - if(d && d.id) { tmp.id = d.id.toString(); } - if(d && d.text) { tmp.text = d.text; } - if(d && d.data && d.data.jstree && d.data.jstree.icon) { - tmp.icon = d.data.jstree.icon; - } - if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { - tmp.icon = true; - } - if(d && d.data) { - tmp.data = d.data; - if(d.data.jstree) { - for(i in d.data.jstree) { - if(d.data.jstree.hasOwnProperty(i)) { - tmp.state[i] = d.data.jstree[i]; - } - } - } - } - if(d && typeof d.state === 'object') { - for (i in d.state) { - if(d.state.hasOwnProperty(i)) { - tmp.state[i] = d.state[i]; - } - } - } - if(d && typeof d.li_attr === 'object') { - for (i in d.li_attr) { - if(d.li_attr.hasOwnProperty(i)) { - tmp.li_attr[i] = d.li_attr[i]; - } - } - } - if(tmp.li_attr.id && !tmp.id) { - tmp.id = tmp.li_attr.id.toString(); - } - if(!tmp.id) { - tmp.id = tid; - } - if(!tmp.li_attr.id) { - tmp.li_attr.id = tmp.id; - } - if(d && typeof d.a_attr === 'object') { - for (i in d.a_attr) { - if(d.a_attr.hasOwnProperty(i)) { - tmp.a_attr[i] = d.a_attr[i]; - } - } - } - if(d && d.children && d.children.length) { - for(i = 0, j = d.children.length; i < j; i++) { - c = parse_nest(d.children[i], tmp.id, ps); - e = m[c]; - tmp.children.push(c); - if(e.children_d.length) { - tmp.children_d = tmp.children_d.concat(e.children_d); - } - } - tmp.children_d = tmp.children_d.concat(tmp.children); - } - if(d && d.children && d.children === true) { - tmp.state.loaded = false; - tmp.children = []; - tmp.children_d = []; - } - delete d.data; - delete d.children; - tmp.original = d; - m[tmp.id] = tmp; - if(tmp.state.selected) { - add.push(tmp.id); - } - return tmp.id; - }; - - if(dat.length && dat[0].id !== undefined && dat[0].parent !== undefined) { - // Flat JSON support (for easy import from DB): - // 1) convert to object (foreach) - for(i = 0, j = dat.length; i < j; i++) { - if(!dat[i].children) { - dat[i].children = []; - } - m[dat[i].id.toString()] = dat[i]; - } - // 2) populate children (foreach) - for(i = 0, j = dat.length; i < j; i++) { - m[dat[i].parent.toString()].children.push(dat[i].id.toString()); - // populate parent.children_d - p.children_d.push(dat[i].id.toString()); - } - // 3) normalize && populate parents and children_d with recursion - for(i = 0, j = p.children.length; i < j; i++) { - tmp = parse_flat(m[p.children[i]], par, p.parents.concat()); - dpc.push(tmp); - if(m[tmp].children_d.length) { - dpc = dpc.concat(m[tmp].children_d); - } - } - for(i = 0, j = p.parents.length; i < j; i++) { - m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc); - } - // ?) three_state selection - p.state.selected && t - (if three_state foreach(dat => ch) -> foreach(parents) if(parent.selected) child.selected = true; - rslt = { - 'cnt' : t_cnt, - 'mod' : m, - 'sel' : sel, - 'par' : par, - 'dpc' : dpc, - 'add' : add - }; - } - else { - for(i = 0, j = dat.length; i < j; i++) { - tmp = parse_nest(dat[i], par, p.parents.concat()); - if(tmp) { - chd.push(tmp); - dpc.push(tmp); - if(m[tmp].children_d.length) { - dpc = dpc.concat(m[tmp].children_d); - } - } - } - p.children = chd; - p.children_d = dpc; - for(i = 0, j = p.parents.length; i < j; i++) { - m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc); - } - rslt = { - 'cnt' : t_cnt, - 'mod' : m, - 'sel' : sel, - 'par' : par, - 'dpc' : dpc, - 'add' : add - }; - } - if(typeof window === 'undefined' || typeof window.document === 'undefined') { - postMessage(rslt); - } - else { - return rslt; - } - }, - rslt = function (rslt, worker) { - if(this.element === null) { return; } - this._cnt = rslt.cnt; - this._model.data = rslt.mod; // breaks the reference in load_node - careful - - if(worker) { - var i, j, a = rslt.add, r = rslt.sel, s = this._data.core.selected.slice(), m = this._model.data; - // if selection was changed while calculating in worker - if(r.length !== s.length || $.vakata.array_unique(r.concat(s)).length !== r.length) { - // deselect nodes that are no longer selected - for(i = 0, j = r.length; i < j; i++) { - if($.inArray(r[i], a) === -1 && $.inArray(r[i], s) === -1) { - m[r[i]].state.selected = false; - } - } - // select nodes that were selected in the mean time - for(i = 0, j = s.length; i < j; i++) { - if($.inArray(s[i], r) === -1) { - m[s[i]].state.selected = true; - } - } - } - } - if(rslt.add.length) { - this._data.core.selected = this._data.core.selected.concat(rslt.add); - } - - this.trigger('model', { "nodes" : rslt.dpc, 'parent' : rslt.par }); - - if(rslt.par !== $.jstree.root) { - this._node_changed(rslt.par); - this.redraw(); - } - else { - // this.get_container_ul().children('.jstree-initial-node').remove(); - this.redraw(true); - } - if(rslt.add.length) { - this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected }); - } - cb.call(this, true); - }; - if(this.settings.core.worker && window.Blob && window.URL && window.Worker) { - try { - if(this._wrk === null) { - this._wrk = window.URL.createObjectURL( - new window.Blob( - ['self.onmessage = ' + func.toString()], - {type:"text/javascript"} - ) - ); - } - if(!this._data.core.working || force_processing) { - this._data.core.working = true; - w = new window.Worker(this._wrk); - w.onmessage = $.proxy(function (e) { - rslt.call(this, e.data, true); - try { w.terminate(); w = null; } catch(ignore) { } - if(this._data.core.worker_queue.length) { - this._append_json_data.apply(this, this._data.core.worker_queue.shift()); - } - else { - this._data.core.working = false; - } - }, this); - if(!args.par) { - if(this._data.core.worker_queue.length) { - this._append_json_data.apply(this, this._data.core.worker_queue.shift()); - } - else { - this._data.core.working = false; - } - } - else { - w.postMessage(args); - } - } - else { - this._data.core.worker_queue.push([dom, data, cb, true]); - } - } - catch(e) { - rslt.call(this, func(args), false); - if(this._data.core.worker_queue.length) { - this._append_json_data.apply(this, this._data.core.worker_queue.shift()); - } - else { - this._data.core.working = false; - } - } - } - else { - rslt.call(this, func(args), false); - } - }, - /** - * parses a node from a jQuery object and appends them to the in memory tree model. Used internally. - * @private - * @name _parse_model_from_html(d [, p, ps]) - * @param {jQuery} d the jQuery object to parse - * @param {String} p the parent ID - * @param {Array} ps list of all parents - * @return {String} the ID of the object added to the model - */ - _parse_model_from_html : function (d, p, ps) { - if(!ps) { ps = []; } - else { ps = [].concat(ps); } - if(p) { ps.unshift(p); } - var c, e, m = this._model.data, - data = { - id : false, - text : false, - icon : true, - parent : p, - parents : ps, - children : [], - children_d : [], - data : null, - state : { }, - li_attr : { id : false }, - a_attr : { href : '#' }, - original : false - }, i, tmp, tid; - for(i in this._model.default_state) { - if(this._model.default_state.hasOwnProperty(i)) { - data.state[i] = this._model.default_state[i]; - } - } - tmp = $.vakata.attributes(d, true); - $.each(tmp, function (i, v) { - v = $.trim(v); - if(!v.length) { return true; } - data.li_attr[i] = v; - if(i === 'id') { - data.id = v.toString(); - } - }); - tmp = d.children('a').first(); - if(tmp.length) { - tmp = $.vakata.attributes(tmp, true); - $.each(tmp, function (i, v) { - v = $.trim(v); - if(v.length) { - data.a_attr[i] = v; - } - }); - } - tmp = d.children("a").first().length ? d.children("a").first().clone() : d.clone(); - tmp.children("ins, i, ul").remove(); - tmp = tmp.html(); - tmp = $('
    ').html(tmp); - data.text = this.settings.core.force_text ? tmp.text() : tmp.html(); - tmp = d.data(); - data.data = tmp ? $.extend(true, {}, tmp) : null; - data.state.opened = d.hasClass('jstree-open'); - data.state.selected = d.children('a').hasClass('jstree-clicked'); - data.state.disabled = d.children('a').hasClass('jstree-disabled'); - if(data.data && data.data.jstree) { - for(i in data.data.jstree) { - if(data.data.jstree.hasOwnProperty(i)) { - data.state[i] = data.data.jstree[i]; - } - } - } - tmp = d.children("a").children(".jstree-themeicon"); - if(tmp.length) { - data.icon = tmp.hasClass('jstree-themeicon-hidden') ? false : tmp.attr('rel'); - } - if(data.state.icon !== undefined) { - data.icon = data.state.icon; - } - if(data.icon === undefined || data.icon === null || data.icon === "") { - data.icon = true; - } - tmp = d.children("ul").children("li"); - do { - tid = 'j' + this._id + '_' + (++this._cnt); - } while(m[tid]); - data.id = data.li_attr.id ? data.li_attr.id.toString() : tid; - if(tmp.length) { - tmp.each($.proxy(function (i, v) { - c = this._parse_model_from_html($(v), data.id, ps); - e = this._model.data[c]; - data.children.push(c); - if(e.children_d.length) { - data.children_d = data.children_d.concat(e.children_d); - } - }, this)); - data.children_d = data.children_d.concat(data.children); - } - else { - if(d.hasClass('jstree-closed')) { - data.state.loaded = false; - } - } - if(data.li_attr['class']) { - data.li_attr['class'] = data.li_attr['class'].replace('jstree-closed','').replace('jstree-open',''); - } - if(data.a_attr['class']) { - data.a_attr['class'] = data.a_attr['class'].replace('jstree-clicked','').replace('jstree-disabled',''); - } - m[data.id] = data; - if(data.state.selected) { - this._data.core.selected.push(data.id); - } - return data.id; - }, - /** - * parses a node from a JSON object (used when dealing with flat data, which has no nesting of children, but has id and parent properties) and appends it to the in memory tree model. Used internally. - * @private - * @name _parse_model_from_flat_json(d [, p, ps]) - * @param {Object} d the JSON object to parse - * @param {String} p the parent ID - * @param {Array} ps list of all parents - * @return {String} the ID of the object added to the model - */ - _parse_model_from_flat_json : function (d, p, ps) { - if(!ps) { ps = []; } - else { ps = ps.concat(); } - if(p) { ps.unshift(p); } - var tid = d.id.toString(), - m = this._model.data, - df = this._model.default_state, - i, j, c, e, - tmp = { - id : tid, - text : d.text || '', - icon : d.icon !== undefined ? d.icon : true, - parent : p, - parents : ps, - children : d.children || [], - children_d : d.children_d || [], - data : d.data, - state : { }, - li_attr : { id : false }, - a_attr : { href : '#' }, - original : false - }; - for(i in df) { - if(df.hasOwnProperty(i)) { - tmp.state[i] = df[i]; - } - } - if(d && d.data && d.data.jstree && d.data.jstree.icon) { - tmp.icon = d.data.jstree.icon; - } - if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { - tmp.icon = true; - } - if(d && d.data) { - tmp.data = d.data; - if(d.data.jstree) { - for(i in d.data.jstree) { - if(d.data.jstree.hasOwnProperty(i)) { - tmp.state[i] = d.data.jstree[i]; - } - } - } - } - if(d && typeof d.state === 'object') { - for (i in d.state) { - if(d.state.hasOwnProperty(i)) { - tmp.state[i] = d.state[i]; - } - } - } - if(d && typeof d.li_attr === 'object') { - for (i in d.li_attr) { - if(d.li_attr.hasOwnProperty(i)) { - tmp.li_attr[i] = d.li_attr[i]; - } - } - } - if(!tmp.li_attr.id) { - tmp.li_attr.id = tid; - } - if(d && typeof d.a_attr === 'object') { - for (i in d.a_attr) { - if(d.a_attr.hasOwnProperty(i)) { - tmp.a_attr[i] = d.a_attr[i]; - } - } - } - if(d && d.children && d.children === true) { - tmp.state.loaded = false; - tmp.children = []; - tmp.children_d = []; - } - m[tmp.id] = tmp; - for(i = 0, j = tmp.children.length; i < j; i++) { - c = this._parse_model_from_flat_json(m[tmp.children[i]], tmp.id, ps); - e = m[c]; - tmp.children_d.push(c); - if(e.children_d.length) { - tmp.children_d = tmp.children_d.concat(e.children_d); - } - } - delete d.data; - delete d.children; - m[tmp.id].original = d; - if(tmp.state.selected) { - this._data.core.selected.push(tmp.id); - } - return tmp.id; - }, - /** - * parses a node from a JSON object and appends it to the in memory tree model. Used internally. - * @private - * @name _parse_model_from_json(d [, p, ps]) - * @param {Object} d the JSON object to parse - * @param {String} p the parent ID - * @param {Array} ps list of all parents - * @return {String} the ID of the object added to the model - */ - _parse_model_from_json : function (d, p, ps) { - if(!ps) { ps = []; } - else { ps = ps.concat(); } - if(p) { ps.unshift(p); } - var tid = false, i, j, c, e, m = this._model.data, df = this._model.default_state, tmp; - do { - tid = 'j' + this._id + '_' + (++this._cnt); - } while(m[tid]); - - tmp = { - id : false, - text : typeof d === 'string' ? d : '', - icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true, - parent : p, - parents : ps, - children : [], - children_d : [], - data : null, - state : { }, - li_attr : { id : false }, - a_attr : { href : '#' }, - original : false - }; - for(i in df) { - if(df.hasOwnProperty(i)) { - tmp.state[i] = df[i]; - } - } - if(d && d.id) { tmp.id = d.id.toString(); } - if(d && d.text) { tmp.text = d.text; } - if(d && d.data && d.data.jstree && d.data.jstree.icon) { - tmp.icon = d.data.jstree.icon; - } - if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { - tmp.icon = true; - } - if(d && d.data) { - tmp.data = d.data; - if(d.data.jstree) { - for(i in d.data.jstree) { - if(d.data.jstree.hasOwnProperty(i)) { - tmp.state[i] = d.data.jstree[i]; - } - } - } - } - if(d && typeof d.state === 'object') { - for (i in d.state) { - if(d.state.hasOwnProperty(i)) { - tmp.state[i] = d.state[i]; - } - } - } - if(d && typeof d.li_attr === 'object') { - for (i in d.li_attr) { - if(d.li_attr.hasOwnProperty(i)) { - tmp.li_attr[i] = d.li_attr[i]; - } - } - } - if(tmp.li_attr.id && !tmp.id) { - tmp.id = tmp.li_attr.id.toString(); - } - if(!tmp.id) { - tmp.id = tid; - } - if(!tmp.li_attr.id) { - tmp.li_attr.id = tmp.id; - } - if(d && typeof d.a_attr === 'object') { - for (i in d.a_attr) { - if(d.a_attr.hasOwnProperty(i)) { - tmp.a_attr[i] = d.a_attr[i]; - } - } - } - if(d && d.children && d.children.length) { - for(i = 0, j = d.children.length; i < j; i++) { - c = this._parse_model_from_json(d.children[i], tmp.id, ps); - e = m[c]; - tmp.children.push(c); - if(e.children_d.length) { - tmp.children_d = tmp.children_d.concat(e.children_d); - } - } - tmp.children_d = tmp.children_d.concat(tmp.children); - } - if(d && d.children && d.children === true) { - tmp.state.loaded = false; - tmp.children = []; - tmp.children_d = []; - } - delete d.data; - delete d.children; - tmp.original = d; - m[tmp.id] = tmp; - if(tmp.state.selected) { - this._data.core.selected.push(tmp.id); - } - return tmp.id; - }, - /** - * redraws all nodes that need to be redrawn. Used internally. - * @private - * @name _redraw() - * @trigger redraw.jstree - */ - _redraw : function () { - var nodes = this._model.force_full_redraw ? this._model.data[$.jstree.root].children.concat([]) : this._model.changed.concat([]), - f = document.createElement('UL'), tmp, i, j, fe = this._data.core.focused; - for(i = 0, j = nodes.length; i < j; i++) { - tmp = this.redraw_node(nodes[i], true, this._model.force_full_redraw); - if(tmp && this._model.force_full_redraw) { - f.appendChild(tmp); - } - } - if(this._model.force_full_redraw) { - f.className = this.get_container_ul()[0].className; - f.setAttribute('role','group'); - this.element.empty().append(f); - //this.get_container_ul()[0].appendChild(f); - } - if(fe !== null) { - tmp = this.get_node(fe, true); - if(tmp && tmp.length && tmp.children('.jstree-anchor')[0] !== document.activeElement) { - tmp.children('.jstree-anchor').focus(); - } - else { - this._data.core.focused = null; - } - } - this._model.force_full_redraw = false; - this._model.changed = []; - /** - * triggered after nodes are redrawn - * @event - * @name redraw.jstree - * @param {array} nodes the redrawn nodes - */ - this.trigger('redraw', { "nodes" : nodes }); - }, - /** - * redraws all nodes that need to be redrawn or optionally - the whole tree - * @name redraw([full]) - * @param {Boolean} full if set to `true` all nodes are redrawn. - */ - redraw : function (full) { - if(full) { - this._model.force_full_redraw = true; - } - //if(this._model.redraw_timeout) { - // clearTimeout(this._model.redraw_timeout); - //} - //this._model.redraw_timeout = setTimeout($.proxy(this._redraw, this),0); - this._redraw(); - }, - /** - * redraws a single node's children. Used internally. - * @private - * @name draw_children(node) - * @param {mixed} node the node whose children will be redrawn - */ - draw_children : function (node) { - var obj = this.get_node(node), - i = false, - j = false, - k = false, - d = document; - if(!obj) { return false; } - if(obj.id === $.jstree.root) { return this.redraw(true); } - node = this.get_node(node, true); - if(!node || !node.length) { return false; } // TODO: quick toggle - - node.children('.jstree-children').remove(); - node = node[0]; - if(obj.children.length && obj.state.loaded) { - k = d.createElement('UL'); - k.setAttribute('role', 'group'); - k.className = 'jstree-children'; - for(i = 0, j = obj.children.length; i < j; i++) { - k.appendChild(this.redraw_node(obj.children[i], true, true)); - } - node.appendChild(k); - } - }, - /** - * redraws a single node. Used internally. - * @private - * @name redraw_node(node, deep, is_callback, force_render) - * @param {mixed} node the node to redraw - * @param {Boolean} deep should child nodes be redrawn too - * @param {Boolean} is_callback is this a recursion call - * @param {Boolean} force_render should children of closed parents be drawn anyway - */ - redraw_node : function (node, deep, is_callback, force_render) { - var obj = this.get_node(node), - par = false, - ind = false, - old = false, - i = false, - j = false, - k = false, - c = '', - d = document, - m = this._model.data, - f = false, - s = false, - tmp = null, - t = 0, - l = 0, - has_children = false, - last_sibling = false; - if(!obj) { return false; } - if(obj.id === $.jstree.root) { return this.redraw(true); } - deep = deep || obj.children.length === 0; - node = !document.querySelector ? document.getElementById(obj.id) : this.element[0].querySelector('#' + ("0123456789".indexOf(obj.id[0]) !== -1 ? '\\3' + obj.id[0] + ' ' + obj.id.substr(1).replace($.jstree.idregex,'\\$&') : obj.id.replace($.jstree.idregex,'\\$&')) ); //, this.element); - if(!node) { - deep = true; - //node = d.createElement('LI'); - if(!is_callback) { - par = obj.parent !== $.jstree.root ? $('#' + obj.parent.replace($.jstree.idregex,'\\$&'), this.element)[0] : null; - if(par !== null && (!par || !m[obj.parent].state.opened)) { - return false; - } - ind = $.inArray(obj.id, par === null ? m[$.jstree.root].children : m[obj.parent].children); - } - } - else { - node = $(node); - if(!is_callback) { - par = node.parent().parent()[0]; - if(par === this.element[0]) { - par = null; - } - ind = node.index(); - } - // m[obj.id].data = node.data(); // use only node's data, no need to touch jquery storage - if(!deep && obj.children.length && !node.children('.jstree-children').length) { - deep = true; - } - if(!deep) { - old = node.children('.jstree-children')[0]; - } - f = node.children('.jstree-anchor')[0] === document.activeElement; - node.remove(); - //node = d.createElement('LI'); - //node = node[0]; - } - node = _node.cloneNode(true); - // node is DOM, deep is boolean - - c = 'jstree-node '; - for(i in obj.li_attr) { - if(obj.li_attr.hasOwnProperty(i)) { - if(i === 'id') { continue; } - if(i !== 'class') { - node.setAttribute(i, obj.li_attr[i]); - } - else { - c += obj.li_attr[i]; - } - } - } - if(!obj.a_attr.id) { - obj.a_attr.id = obj.id + '_anchor'; - } - node.setAttribute('aria-selected', !!obj.state.selected); - node.setAttribute('aria-level', obj.parents.length); - node.setAttribute('aria-labelledby', obj.a_attr.id); - if(obj.state.disabled) { - node.setAttribute('aria-disabled', true); - } - - for(i = 0, j = obj.children.length; i < j; i++) { - if(!m[obj.children[i]].state.hidden) { - has_children = true; - break; - } - } - if(obj.parent !== null && m[obj.parent] && !obj.state.hidden) { - i = $.inArray(obj.id, m[obj.parent].children); - last_sibling = obj.id; - if(i !== -1) { - i++; - for(j = m[obj.parent].children.length; i < j; i++) { - if(!m[m[obj.parent].children[i]].state.hidden) { - last_sibling = m[obj.parent].children[i]; - } - if(last_sibling !== obj.id) { - break; - } - } - } - } - - if(obj.state.hidden) { - c += ' jstree-hidden'; - } - if(obj.state.loaded && !has_children) { - c += ' jstree-leaf'; - } - else { - c += obj.state.opened && obj.state.loaded ? ' jstree-open' : ' jstree-closed'; - node.setAttribute('aria-expanded', (obj.state.opened && obj.state.loaded) ); - } - if(last_sibling === obj.id) { - c += ' jstree-last'; - } - node.id = obj.id; - node.className = c; - c = ( obj.state.selected ? ' jstree-clicked' : '') + ( obj.state.disabled ? ' jstree-disabled' : ''); - for(j in obj.a_attr) { - if(obj.a_attr.hasOwnProperty(j)) { - if(j === 'href' && obj.a_attr[j] === '#') { continue; } - if(j !== 'class') { - node.childNodes[1].setAttribute(j, obj.a_attr[j]); - } - else { - c += ' ' + obj.a_attr[j]; - } - } - } - if(c.length) { - node.childNodes[1].className = 'jstree-anchor ' + c; - } - if((obj.icon && obj.icon !== true) || obj.icon === false) { - if(obj.icon === false) { - node.childNodes[1].childNodes[0].className += ' jstree-themeicon-hidden'; - } - else if(obj.icon.indexOf('/') === -1 && obj.icon.indexOf('.') === -1) { - node.childNodes[1].childNodes[0].className += ' ' + obj.icon + ' jstree-themeicon-custom'; - } - else { - node.childNodes[1].childNodes[0].style.backgroundImage = 'url('+obj.icon+')'; - node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center'; - node.childNodes[1].childNodes[0].style.backgroundSize = 'auto'; - node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom'; - } - } - - if(this.settings.core.force_text) { - node.childNodes[1].appendChild(d.createTextNode(obj.text)); - } - else { - node.childNodes[1].innerHTML += obj.text; - } - - - if(deep && obj.children.length && (obj.state.opened || force_render) && obj.state.loaded) { - k = d.createElement('UL'); - k.setAttribute('role', 'group'); - k.className = 'jstree-children'; - for(i = 0, j = obj.children.length; i < j; i++) { - k.appendChild(this.redraw_node(obj.children[i], deep, true)); - } - node.appendChild(k); - } - if(old) { - node.appendChild(old); - } - if(!is_callback) { - // append back using par / ind - if(!par) { - par = this.element[0]; - } - for(i = 0, j = par.childNodes.length; i < j; i++) { - if(par.childNodes[i] && par.childNodes[i].className && par.childNodes[i].className.indexOf('jstree-children') !== -1) { - tmp = par.childNodes[i]; - break; - } - } - if(!tmp) { - tmp = d.createElement('UL'); - tmp.setAttribute('role', 'group'); - tmp.className = 'jstree-children'; - par.appendChild(tmp); - } - par = tmp; - - if(ind < par.childNodes.length) { - par.insertBefore(node, par.childNodes[ind]); - } - else { - par.appendChild(node); - } - if(f) { - t = this.element[0].scrollTop; - l = this.element[0].scrollLeft; - node.childNodes[1].focus(); - this.element[0].scrollTop = t; - this.element[0].scrollLeft = l; - } - } - if(obj.state.opened && !obj.state.loaded) { - obj.state.opened = false; - setTimeout($.proxy(function () { - this.open_node(obj.id, false, 0); - }, this), 0); - } - return node; - }, - /** - * opens a node, revaling its children. If the node is not loaded it will be loaded and opened once ready. - * @name open_node(obj [, callback, animation]) - * @param {mixed} obj the node to open - * @param {Function} callback a function to execute once the node is opened - * @param {Number} animation the animation duration in milliseconds when opening the node (overrides the `core.animation` setting). Use `false` for no animation. - * @trigger open_node.jstree, after_open.jstree, before_open.jstree - */ - open_node : function (obj, callback, animation) { - var t1, t2, d, t; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.open_node(obj[t1], callback, animation); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - animation = animation === undefined ? this.settings.core.animation : animation; - if(!this.is_closed(obj)) { - if(callback) { - callback.call(this, obj, false); - } - return false; - } - if(!this.is_loaded(obj)) { - if(this.is_loading(obj)) { - return setTimeout($.proxy(function () { - this.open_node(obj, callback, animation); - }, this), 500); - } - this.load_node(obj, function (o, ok) { - return ok ? this.open_node(o, callback, animation) : (callback ? callback.call(this, o, false) : false); - }); - } - else { - d = this.get_node(obj, true); - t = this; - if(d.length) { - if(animation && d.children(".jstree-children").length) { - d.children(".jstree-children").stop(true, true); - } - if(obj.children.length && !this._firstChild(d.children('.jstree-children')[0])) { - this.draw_children(obj); - //d = this.get_node(obj, true); - } - if(!animation) { - this.trigger('before_open', { "node" : obj }); - d[0].className = d[0].className.replace('jstree-closed', 'jstree-open'); - d[0].setAttribute("aria-expanded", true); - } - else { - this.trigger('before_open', { "node" : obj }); - d - .children(".jstree-children").css("display","none").end() - .removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true) - .children(".jstree-children").stop(true, true) - .slideDown(animation, function () { - this.style.display = ""; - t.trigger("after_open", { "node" : obj }); - }); - } - } - obj.state.opened = true; - if(callback) { - callback.call(this, obj, true); - } - if(!d.length) { - /** - * triggered when a node is about to be opened (if the node is supposed to be in the DOM, it will be, but it won't be visible yet) - * @event - * @name before_open.jstree - * @param {Object} node the opened node - */ - this.trigger('before_open', { "node" : obj }); - } - /** - * triggered when a node is opened (if there is an animation it will not be completed yet) - * @event - * @name open_node.jstree - * @param {Object} node the opened node - */ - this.trigger('open_node', { "node" : obj }); - if(!animation || !d.length) { - /** - * triggered when a node is opened and the animation is complete - * @event - * @name after_open.jstree - * @param {Object} node the opened node - */ - this.trigger("after_open", { "node" : obj }); - } - return true; - } - }, - /** - * opens every parent of a node (node should be loaded) - * @name _open_to(obj) - * @param {mixed} obj the node to reveal - * @private - */ - _open_to : function (obj) { - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - var i, j, p = obj.parents; - for(i = 0, j = p.length; i < j; i+=1) { - if(i !== $.jstree.root) { - this.open_node(p[i], false, 0); - } - } - return $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element); - }, - /** - * closes a node, hiding its children - * @name close_node(obj [, animation]) - * @param {mixed} obj the node to close - * @param {Number} animation the animation duration in milliseconds when closing the node (overrides the `core.animation` setting). Use `false` for no animation. - * @trigger close_node.jstree, after_close.jstree - */ - close_node : function (obj, animation) { - var t1, t2, t, d; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.close_node(obj[t1], animation); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - if(this.is_closed(obj)) { - return false; - } - animation = animation === undefined ? this.settings.core.animation : animation; - t = this; - d = this.get_node(obj, true); - if(d.length) { - if(!animation) { - d[0].className = d[0].className.replace('jstree-open', 'jstree-closed'); - d.attr("aria-expanded", false).children('.jstree-children').remove(); - } - else { - d - .children(".jstree-children").attr("style","display:block !important").end() - .removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false) - .children(".jstree-children").stop(true, true).slideUp(animation, function () { - this.style.display = ""; - d.children('.jstree-children').remove(); - t.trigger("after_close", { "node" : obj }); - }); - } - } - obj.state.opened = false; - /** - * triggered when a node is closed (if there is an animation it will not be complete yet) - * @event - * @name close_node.jstree - * @param {Object} node the closed node - */ - this.trigger('close_node',{ "node" : obj }); - if(!animation || !d.length) { - /** - * triggered when a node is closed and the animation is complete - * @event - * @name after_close.jstree - * @param {Object} node the closed node - */ - this.trigger("after_close", { "node" : obj }); - } - }, - /** - * toggles a node - closing it if it is open, opening it if it is closed - * @name toggle_node(obj) - * @param {mixed} obj the node to toggle - */ - toggle_node : function (obj) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.toggle_node(obj[t1]); - } - return true; - } - if(this.is_closed(obj)) { - return this.open_node(obj); - } - if(this.is_open(obj)) { - return this.close_node(obj); - } - }, - /** - * opens all nodes within a node (or the tree), revaling their children. If the node is not loaded it will be loaded and opened once ready. - * @name open_all([obj, animation, original_obj]) - * @param {mixed} obj the node to open recursively, omit to open all nodes in the tree - * @param {Number} animation the animation duration in milliseconds when opening the nodes, the default is no animation - * @param {jQuery} reference to the node that started the process (internal use) - * @trigger open_all.jstree - */ - open_all : function (obj, animation, original_obj) { - if(!obj) { obj = $.jstree.root; } - obj = this.get_node(obj); - if(!obj) { return false; } - var dom = obj.id === $.jstree.root ? this.get_container_ul() : this.get_node(obj, true), i, j, _this; - if(!dom.length) { - for(i = 0, j = obj.children_d.length; i < j; i++) { - if(this.is_closed(this._model.data[obj.children_d[i]])) { - this._model.data[obj.children_d[i]].state.opened = true; - } - } - return this.trigger('open_all', { "node" : obj }); - } - original_obj = original_obj || dom; - _this = this; - dom = this.is_closed(obj) ? dom.find('.jstree-closed').addBack() : dom.find('.jstree-closed'); - dom.each(function () { - _this.open_node( - this, - function(node, status) { if(status && this.is_parent(node)) { this.open_all(node, animation, original_obj); } }, - animation || 0 - ); - }); - if(original_obj.find('.jstree-closed').length === 0) { - /** - * triggered when an `open_all` call completes - * @event - * @name open_all.jstree - * @param {Object} node the opened node - */ - this.trigger('open_all', { "node" : this.get_node(original_obj) }); - } - }, - /** - * closes all nodes within a node (or the tree), revaling their children - * @name close_all([obj, animation]) - * @param {mixed} obj the node to close recursively, omit to close all nodes in the tree - * @param {Number} animation the animation duration in milliseconds when closing the nodes, the default is no animation - * @trigger close_all.jstree - */ - close_all : function (obj, animation) { - if(!obj) { obj = $.jstree.root; } - obj = this.get_node(obj); - if(!obj) { return false; } - var dom = obj.id === $.jstree.root ? this.get_container_ul() : this.get_node(obj, true), - _this = this, i, j; - if(dom.length) { - dom = this.is_open(obj) ? dom.find('.jstree-open').addBack() : dom.find('.jstree-open'); - $(dom.get().reverse()).each(function () { _this.close_node(this, animation || 0); }); - } - for(i = 0, j = obj.children_d.length; i < j; i++) { - this._model.data[obj.children_d[i]].state.opened = false; - } - /** - * triggered when an `close_all` call completes - * @event - * @name close_all.jstree - * @param {Object} node the closed node - */ - this.trigger('close_all', { "node" : obj }); - }, - /** - * checks if a node is disabled (not selectable) - * @name is_disabled(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_disabled : function (obj) { - obj = this.get_node(obj); - return obj && obj.state && obj.state.disabled; - }, - /** - * enables a node - so that it can be selected - * @name enable_node(obj) - * @param {mixed} obj the node to enable - * @trigger enable_node.jstree - */ - enable_node : function (obj) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.enable_node(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - obj.state.disabled = false; - this.get_node(obj,true).children('.jstree-anchor').removeClass('jstree-disabled').attr('aria-disabled', false); - /** - * triggered when an node is enabled - * @event - * @name enable_node.jstree - * @param {Object} node the enabled node - */ - this.trigger('enable_node', { 'node' : obj }); - }, - /** - * disables a node - so that it can not be selected - * @name disable_node(obj) - * @param {mixed} obj the node to disable - * @trigger disable_node.jstree - */ - disable_node : function (obj) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.disable_node(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - obj.state.disabled = true; - this.get_node(obj,true).children('.jstree-anchor').addClass('jstree-disabled').attr('aria-disabled', true); - /** - * triggered when an node is disabled - * @event - * @name disable_node.jstree - * @param {Object} node the disabled node - */ - this.trigger('disable_node', { 'node' : obj }); - }, - /** - * hides a node - it is still in the structure but will not be visible - * @name hide_node(obj) - * @param {mixed} obj the node to hide - * @param {Boolean} redraw internal parameter controlling if redraw is called - * @trigger hide_node.jstree - */ - hide_node : function (obj, skip_redraw) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.hide_node(obj[t1], true); - } - this.redraw(); - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - if(!obj.state.hidden) { - obj.state.hidden = true; - this._node_changed(obj.parent); - if(!skip_redraw) { - this.redraw(); - } - /** - * triggered when an node is hidden - * @event - * @name hide_node.jstree - * @param {Object} node the hidden node - */ - this.trigger('hide_node', { 'node' : obj }); - } - }, - /** - * shows a node - * @name show_node(obj) - * @param {mixed} obj the node to show - * @param {Boolean} skip_redraw internal parameter controlling if redraw is called - * @trigger show_node.jstree - */ - show_node : function (obj, skip_redraw) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.show_node(obj[t1], true); - } - this.redraw(); - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - if(obj.state.hidden) { - obj.state.hidden = false; - this._node_changed(obj.parent); - if(!skip_redraw) { - this.redraw(); - } - /** - * triggered when an node is shown - * @event - * @name show_node.jstree - * @param {Object} node the shown node - */ - this.trigger('show_node', { 'node' : obj }); - } - }, - /** - * hides all nodes - * @name hide_all() - * @trigger hide_all.jstree - */ - hide_all : function (skip_redraw) { - var i, m = this._model.data, ids = []; - for(i in m) { - if(m.hasOwnProperty(i) && i !== $.jstree.root && !m[i].state.hidden) { - m[i].state.hidden = true; - ids.push(i); - } - } - this._model.force_full_redraw = true; - if(!skip_redraw) { - this.redraw(); - } - /** - * triggered when all nodes are hidden - * @event - * @name hide_all.jstree - * @param {Array} nodes the IDs of all hidden nodes - */ - this.trigger('hide_all', { 'nodes' : ids }); - return ids; - }, - /** - * shows all nodes - * @name show_all() - * @trigger show_all.jstree - */ - show_all : function (skip_redraw) { - var i, m = this._model.data, ids = []; - for(i in m) { - if(m.hasOwnProperty(i) && i !== $.jstree.root && m[i].state.hidden) { - m[i].state.hidden = false; - ids.push(i); - } - } - this._model.force_full_redraw = true; - if(!skip_redraw) { - this.redraw(); - } - /** - * triggered when all nodes are shown - * @event - * @name show_all.jstree - * @param {Array} nodes the IDs of all shown nodes - */ - this.trigger('show_all', { 'nodes' : ids }); - return ids; - }, - /** - * called when a node is selected by the user. Used internally. - * @private - * @name activate_node(obj, e) - * @param {mixed} obj the node - * @param {Object} e the related event - * @trigger activate_node.jstree, changed.jstree - */ - activate_node : function (obj, e) { - if(this.is_disabled(obj)) { - return false; - } - if(!e || typeof e !== 'object') { - e = {}; - } - - // ensure last_clicked is still in the DOM, make it fresh (maybe it was moved?) and make sure it is still selected, if not - make last_clicked the last selected node - this._data.core.last_clicked = this._data.core.last_clicked && this._data.core.last_clicked.id !== undefined ? this.get_node(this._data.core.last_clicked.id) : null; - if(this._data.core.last_clicked && !this._data.core.last_clicked.state.selected) { this._data.core.last_clicked = null; } - if(!this._data.core.last_clicked && this._data.core.selected.length) { this._data.core.last_clicked = this.get_node(this._data.core.selected[this._data.core.selected.length - 1]); } - - if(!this.settings.core.multiple || (!e.metaKey && !e.ctrlKey && !e.shiftKey) || (e.shiftKey && (!this._data.core.last_clicked || !this.get_parent(obj) || this.get_parent(obj) !== this._data.core.last_clicked.parent ) )) { - if(!this.settings.core.multiple && (e.metaKey || e.ctrlKey || e.shiftKey) && this.is_selected(obj)) { - this.deselect_node(obj, false, e); - } - else { - this.deselect_all(true); - this.select_node(obj, false, false, e); - this._data.core.last_clicked = this.get_node(obj); - } - } - else { - if(e.shiftKey) { - var o = this.get_node(obj).id, - l = this._data.core.last_clicked.id, - p = this.get_node(this._data.core.last_clicked.parent).children, - c = false, - i, j; - for(i = 0, j = p.length; i < j; i += 1) { - // separate IFs work whem o and l are the same - if(p[i] === o) { - c = !c; - } - if(p[i] === l) { - c = !c; - } - if(!this.is_disabled(p[i]) && (c || p[i] === o || p[i] === l)) { - this.select_node(p[i], true, false, e); - } - else { - this.deselect_node(p[i], true, e); - } - } - this.trigger('changed', { 'action' : 'select_node', 'node' : this.get_node(obj), 'selected' : this._data.core.selected, 'event' : e }); - } - else { - if(!this.is_selected(obj)) { - this.select_node(obj, false, false, e); - } - else { - this.deselect_node(obj, false, e); - } - } - } - /** - * triggered when an node is clicked or intercated with by the user - * @event - * @name activate_node.jstree - * @param {Object} node - * @param {Object} event the ooriginal event (if any) which triggered the call (may be an empty object) - */ - this.trigger('activate_node', { 'node' : this.get_node(obj), 'event' : e }); - }, - /** - * applies the hover state on a node, called when a node is hovered by the user. Used internally. - * @private - * @name hover_node(obj) - * @param {mixed} obj - * @trigger hover_node.jstree - */ - hover_node : function (obj) { - obj = this.get_node(obj, true); - if(!obj || !obj.length || obj.children('.jstree-hovered').length) { - return false; - } - var o = this.element.find('.jstree-hovered'), t = this.element; - if(o && o.length) { this.dehover_node(o); } - - obj.children('.jstree-anchor').addClass('jstree-hovered'); - /** - * triggered when an node is hovered - * @event - * @name hover_node.jstree - * @param {Object} node - */ - this.trigger('hover_node', { 'node' : this.get_node(obj) }); - setTimeout(function () { t.attr('aria-activedescendant', obj[0].id); }, 0); - }, - /** - * removes the hover state from a nodecalled when a node is no longer hovered by the user. Used internally. - * @private - * @name dehover_node(obj) - * @param {mixed} obj - * @trigger dehover_node.jstree - */ - dehover_node : function (obj) { - obj = this.get_node(obj, true); - if(!obj || !obj.length || !obj.children('.jstree-hovered').length) { - return false; - } - obj.children('.jstree-anchor').removeClass('jstree-hovered'); - /** - * triggered when an node is no longer hovered - * @event - * @name dehover_node.jstree - * @param {Object} node - */ - this.trigger('dehover_node', { 'node' : this.get_node(obj) }); - }, - /** - * select a node - * @name select_node(obj [, supress_event, prevent_open]) - * @param {mixed} obj an array can be used to select multiple nodes - * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered - * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened - * @trigger select_node.jstree, changed.jstree - */ - select_node : function (obj, supress_event, prevent_open, e) { - var dom, t1, t2, th; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.select_node(obj[t1], supress_event, prevent_open, e); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - dom = this.get_node(obj, true); - if(!obj.state.selected) { - obj.state.selected = true; - this._data.core.selected.push(obj.id); - if(!prevent_open) { - dom = this._open_to(obj); - } - if(dom && dom.length) { - dom.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked'); - } - /** - * triggered when an node is selected - * @event - * @name select_node.jstree - * @param {Object} node - * @param {Array} selected the current selection - * @param {Object} event the event (if any) that triggered this select_node - */ - this.trigger('select_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); - if(!supress_event) { - /** - * triggered when selection changes - * @event - * @name changed.jstree - * @param {Object} node - * @param {Object} action the action that caused the selection to change - * @param {Array} selected the current selection - * @param {Object} event the event (if any) that triggered this changed event - */ - this.trigger('changed', { 'action' : 'select_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); - } - } - }, - /** - * deselect a node - * @name deselect_node(obj [, supress_event]) - * @param {mixed} obj an array can be used to deselect multiple nodes - * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered - * @trigger deselect_node.jstree, changed.jstree - */ - deselect_node : function (obj, supress_event, e) { - var t1, t2, dom; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.deselect_node(obj[t1], supress_event, e); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - dom = this.get_node(obj, true); - if(obj.state.selected) { - obj.state.selected = false; - this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.id); - if(dom.length) { - dom.attr('aria-selected', false).children('.jstree-anchor').removeClass('jstree-clicked'); - } - /** - * triggered when an node is deselected - * @event - * @name deselect_node.jstree - * @param {Object} node - * @param {Array} selected the current selection - * @param {Object} event the event (if any) that triggered this deselect_node - */ - this.trigger('deselect_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); - if(!supress_event) { - this.trigger('changed', { 'action' : 'deselect_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); - } - } - }, - /** - * select all nodes in the tree - * @name select_all([supress_event]) - * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered - * @trigger select_all.jstree, changed.jstree - */ - select_all : function (supress_event) { - var tmp = this._data.core.selected.concat([]), i, j; - this._data.core.selected = this._model.data[$.jstree.root].children_d.concat(); - for(i = 0, j = this._data.core.selected.length; i < j; i++) { - if(this._model.data[this._data.core.selected[i]]) { - this._model.data[this._data.core.selected[i]].state.selected = true; - } - } - this.redraw(true); - /** - * triggered when all nodes are selected - * @event - * @name select_all.jstree - * @param {Array} selected the current selection - */ - this.trigger('select_all', { 'selected' : this._data.core.selected }); - if(!supress_event) { - this.trigger('changed', { 'action' : 'select_all', 'selected' : this._data.core.selected, 'old_selection' : tmp }); - } - }, - /** - * deselect all selected nodes - * @name deselect_all([supress_event]) - * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered - * @trigger deselect_all.jstree, changed.jstree - */ - deselect_all : function (supress_event) { - var tmp = this._data.core.selected.concat([]), i, j; - for(i = 0, j = this._data.core.selected.length; i < j; i++) { - if(this._model.data[this._data.core.selected[i]]) { - this._model.data[this._data.core.selected[i]].state.selected = false; - } - } - this._data.core.selected = []; - this.element.find('.jstree-clicked').removeClass('jstree-clicked').parent().attr('aria-selected', false); - /** - * triggered when all nodes are deselected - * @event - * @name deselect_all.jstree - * @param {Object} node the previous selection - * @param {Array} selected the current selection - */ - this.trigger('deselect_all', { 'selected' : this._data.core.selected, 'node' : tmp }); - if(!supress_event) { - this.trigger('changed', { 'action' : 'deselect_all', 'selected' : this._data.core.selected, 'old_selection' : tmp }); - } - }, - /** - * checks if a node is selected - * @name is_selected(obj) - * @param {mixed} obj - * @return {Boolean} - */ - is_selected : function (obj) { - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - return obj.state.selected; - }, - /** - * get an array of all selected nodes - * @name get_selected([full]) - * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned - * @return {Array} - */ - get_selected : function (full) { - return full ? $.map(this._data.core.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.core.selected.slice(); - }, - /** - * get an array of all top level selected nodes (ignoring children of selected nodes) - * @name get_top_selected([full]) - * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned - * @return {Array} - */ - get_top_selected : function (full) { - var tmp = this.get_selected(true), - obj = {}, i, j, k, l; - for(i = 0, j = tmp.length; i < j; i++) { - obj[tmp[i].id] = tmp[i]; - } - for(i = 0, j = tmp.length; i < j; i++) { - for(k = 0, l = tmp[i].children_d.length; k < l; k++) { - if(obj[tmp[i].children_d[k]]) { - delete obj[tmp[i].children_d[k]]; - } - } - } - tmp = []; - for(i in obj) { - if(obj.hasOwnProperty(i)) { - tmp.push(i); - } - } - return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp; - }, - /** - * get an array of all bottom level selected nodes (ignoring selected parents) - * @name get_bottom_selected([full]) - * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned - * @return {Array} - */ - get_bottom_selected : function (full) { - var tmp = this.get_selected(true), - obj = [], i, j; - for(i = 0, j = tmp.length; i < j; i++) { - if(!tmp[i].children.length) { - obj.push(tmp[i].id); - } - } - return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj; - }, - /** - * gets the current state of the tree so that it can be restored later with `set_state(state)`. Used internally. - * @name get_state() - * @private - * @return {Object} - */ - get_state : function () { - var state = { - 'core' : { - 'open' : [], - 'scroll' : { - 'left' : this.element.scrollLeft(), - 'top' : this.element.scrollTop() - }, - /*! - 'themes' : { - 'name' : this.get_theme(), - 'icons' : this._data.core.themes.icons, - 'dots' : this._data.core.themes.dots - }, - */ - 'selected' : [] - } - }, i; - for(i in this._model.data) { - if(this._model.data.hasOwnProperty(i)) { - if(i !== $.jstree.root) { - if(this._model.data[i].state.opened) { - state.core.open.push(i); - } - if(this._model.data[i].state.selected) { - state.core.selected.push(i); - } - } - } - } - return state; - }, - /** - * sets the state of the tree. Used internally. - * @name set_state(state [, callback]) - * @private - * @param {Object} state the state to restore. Keep in mind this object is passed by reference and jstree will modify it. - * @param {Function} callback an optional function to execute once the state is restored. - * @trigger set_state.jstree - */ - set_state : function (state, callback) { - if(state) { - if(state.core) { - var res, n, t, _this, i; - if(state.core.open) { - if(!$.isArray(state.core.open) || !state.core.open.length) { - delete state.core.open; - this.set_state(state, callback); - } - else { - this._load_nodes(state.core.open, function (nodes) { - this.open_node(nodes, false, 0); - delete state.core.open; - this.set_state(state, callback); - }, true); - } - return false; - } - if(state.core.scroll) { - if(state.core.scroll && state.core.scroll.left !== undefined) { - this.element.scrollLeft(state.core.scroll.left); - } - if(state.core.scroll && state.core.scroll.top !== undefined) { - this.element.scrollTop(state.core.scroll.top); - } - delete state.core.scroll; - this.set_state(state, callback); - return false; - } - if(state.core.selected) { - _this = this; - this.deselect_all(); - $.each(state.core.selected, function (i, v) { - _this.select_node(v, false, true); - }); - delete state.core.selected; - this.set_state(state, callback); - return false; - } - for(i in state) { - if(state.hasOwnProperty(i) && i !== "core" && $.inArray(i, this.settings.plugins) === -1) { - delete state[i]; - } - } - if($.isEmptyObject(state.core)) { - delete state.core; - this.set_state(state, callback); - return false; - } - } - if($.isEmptyObject(state)) { - state = null; - if(callback) { callback.call(this); } - /** - * triggered when a `set_state` call completes - * @event - * @name set_state.jstree - */ - this.trigger('set_state'); - return false; - } - return true; - } - return false; - }, - /** - * refreshes the tree - all nodes are reloaded with calls to `load_node`. - * @name refresh() - * @param {Boolean} skip_loading an option to skip showing the loading indicator - * @param {Mixed} forget_state if set to `true` state will not be reapplied, if set to a function (receiving the current state as argument) the result of that function will be used as state - * @trigger refresh.jstree - */ - refresh : function (skip_loading, forget_state) { - this._data.core.state = forget_state === true ? {} : this.get_state(); - if(forget_state && $.isFunction(forget_state)) { this._data.core.state = forget_state.call(this, this._data.core.state); } - this._cnt = 0; - this._model.data = {}; - this._model.data[$.jstree.root] = { - id : $.jstree.root, - parent : null, - parents : [], - children : [], - children_d : [], - state : { loaded : false } - }; - this._data.core.selected = []; - this._data.core.last_clicked = null; - this._data.core.focused = null; - - var c = this.get_container_ul()[0].className; - if(!skip_loading) { - this.element.html("<"+"ul class='"+c+"' role='group'><"+"li class='jstree-initial-node jstree-loading jstree-leaf jstree-last' role='treeitem' id='j"+this._id+"_loading'><"+"a class='jstree-anchor' href='#'>" + this.get_string("Loading ...") + ""); - this.element.attr('aria-activedescendant','j'+this._id+'_loading'); - } - this.load_node($.jstree.root, function (o, s) { - if(s) { - this.get_container_ul()[0].className = c; - if(this._firstChild(this.get_container_ul()[0])) { - this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id); - } - this.set_state($.extend(true, {}, this._data.core.state), function () { - /** - * triggered when a `refresh` call completes - * @event - * @name refresh.jstree - */ - this.trigger('refresh'); - }); - } - this._data.core.state = null; - }); - }, - /** - * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`. - * @name refresh_node(obj) - * @param {mixed} obj the node - * @trigger refresh_node.jstree - */ - refresh_node : function (obj) { - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - var opened = [], to_load = [], s = this._data.core.selected.concat([]); - to_load.push(obj.id); - if(obj.state.opened === true) { opened.push(obj.id); } - this.get_node(obj, true).find('.jstree-open').each(function() { opened.push(this.id); }); - this._load_nodes(to_load, $.proxy(function (nodes) { - this.open_node(opened, false, 0); - this.select_node(this._data.core.selected); - /** - * triggered when a node is refreshed - * @event - * @name refresh_node.jstree - * @param {Object} node - the refreshed node - * @param {Array} nodes - an array of the IDs of the nodes that were reloaded - */ - this.trigger('refresh_node', { 'node' : obj, 'nodes' : nodes }); - }, this)); - }, - /** - * set (change) the ID of a node - * @name set_id(obj, id) - * @param {mixed} obj the node - * @param {String} id the new ID - * @return {Boolean} - */ - set_id : function (obj, id) { - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - var i, j, m = this._model.data; - id = id.toString(); - // update parents (replace current ID with new one in children and children_d) - m[obj.parent].children[$.inArray(obj.id, m[obj.parent].children)] = id; - for(i = 0, j = obj.parents.length; i < j; i++) { - m[obj.parents[i]].children_d[$.inArray(obj.id, m[obj.parents[i]].children_d)] = id; - } - // update children (replace current ID with new one in parent and parents) - for(i = 0, j = obj.children.length; i < j; i++) { - m[obj.children[i]].parent = id; - } - for(i = 0, j = obj.children_d.length; i < j; i++) { - m[obj.children_d[i]].parents[$.inArray(obj.id, m[obj.children_d[i]].parents)] = id; - } - i = $.inArray(obj.id, this._data.core.selected); - if(i !== -1) { this._data.core.selected[i] = id; } - // update model and obj itself (obj.id, this._model.data[KEY]) - i = this.get_node(obj.id, true); - if(i) { - i.attr('id', id).children('.jstree-anchor').attr('id', id + '_anchor').end().attr('aria-labelledby', id + '_anchor'); - if(this.element.attr('aria-activedescendant') === obj.id) { - this.element.attr('aria-activedescendant', id); - } - } - delete m[obj.id]; - obj.id = id; - obj.li_attr.id = id; - m[id] = obj; - return true; - }, - /** - * get the text value of a node - * @name get_text(obj) - * @param {mixed} obj the node - * @return {String} - */ - get_text : function (obj) { - obj = this.get_node(obj); - return (!obj || obj.id === $.jstree.root) ? false : obj.text; - }, - /** - * set the text value of a node. Used internally, please use `rename_node(obj, val)`. - * @private - * @name set_text(obj, val) - * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes - * @param {String} val the new text value - * @return {Boolean} - * @trigger set_text.jstree - */ - set_text : function (obj, val) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.set_text(obj[t1], val); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - obj.text = val; - if(this.get_node(obj, true).length) { - this.redraw_node(obj.id); - } - /** - * triggered when a node text value is changed - * @event - * @name set_text.jstree - * @param {Object} obj - * @param {String} text the new value - */ - this.trigger('set_text',{ "obj" : obj, "text" : val }); - return true; - }, - /** - * gets a JSON representation of a node (or the whole tree) - * @name get_json([obj, options]) - * @param {mixed} obj - * @param {Object} options - * @param {Boolean} options.no_state do not return state information - * @param {Boolean} options.no_id do not return ID - * @param {Boolean} options.no_children do not include children - * @param {Boolean} options.no_data do not include node data - * @param {Boolean} options.flat return flat JSON instead of nested - * @return {Object} - */ - get_json : function (obj, options, flat) { - obj = this.get_node(obj || $.jstree.root); - if(!obj) { return false; } - if(options && options.flat && !flat) { flat = []; } - var tmp = { - 'id' : obj.id, - 'text' : obj.text, - 'icon' : this.get_icon(obj), - 'li_attr' : $.extend(true, {}, obj.li_attr), - 'a_attr' : $.extend(true, {}, obj.a_attr), - 'state' : {}, - 'data' : options && options.no_data ? false : $.extend(true, {}, obj.data) - //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ), - }, i, j; - if(options && options.flat) { - tmp.parent = obj.parent; - } - else { - tmp.children = []; - } - if(!options || !options.no_state) { - for(i in obj.state) { - if(obj.state.hasOwnProperty(i)) { - tmp.state[i] = obj.state[i]; - } - } - } - if(options && options.no_id) { - delete tmp.id; - if(tmp.li_attr && tmp.li_attr.id) { - delete tmp.li_attr.id; - } - if(tmp.a_attr && tmp.a_attr.id) { - delete tmp.a_attr.id; - } - } - if(options && options.flat && obj.id !== $.jstree.root) { - flat.push(tmp); - } - if(!options || !options.no_children) { - for(i = 0, j = obj.children.length; i < j; i++) { - if(options && options.flat) { - this.get_json(obj.children[i], options, flat); - } - else { - tmp.children.push(this.get_json(obj.children[i], options)); - } - } - } - return options && options.flat ? flat : (obj.id === $.jstree.root ? tmp.children : tmp); - }, - /** - * create a new node (do not confuse with load_node) - * @name create_node([obj, node, pos, callback, is_loaded]) - * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`) - * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name) - * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last" - * @param {Function} callback a function to be called once the node is created - * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded - * @return {String} the ID of the newly create node - * @trigger model.jstree, create_node.jstree - */ - create_node : function (par, node, pos, callback, is_loaded) { - if(par === null) { par = $.jstree.root; } - par = this.get_node(par); - if(!par) { return false; } - pos = pos === undefined ? "last" : pos; - if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { - return this.load_node(par, function () { this.create_node(par, node, pos, callback, true); }); - } - if(!node) { node = { "text" : this.get_string('New node') }; } - if(typeof node === "string") { node = { "text" : node }; } - if(node.text === undefined) { node.text = this.get_string('New node'); } - var tmp, dpc, i, j; - - if(par.id === $.jstree.root) { - if(pos === "before") { pos = "first"; } - if(pos === "after") { pos = "last"; } - } - switch(pos) { - case "before": - tmp = this.get_node(par.parent); - pos = $.inArray(par.id, tmp.children); - par = tmp; - break; - case "after" : - tmp = this.get_node(par.parent); - pos = $.inArray(par.id, tmp.children) + 1; - par = tmp; - break; - case "inside": - case "first": - pos = 0; - break; - case "last": - pos = par.children.length; - break; - default: - if(!pos) { pos = 0; } - break; - } - if(pos > par.children.length) { pos = par.children.length; } - if(!node.id) { node.id = true; } - if(!this.check("create_node", node, par, pos)) { - this.settings.core.error.call(this, this._data.core.last_error); - return false; - } - if(node.id === true) { delete node.id; } - node = this._parse_model_from_json(node, par.id, par.parents.concat()); - if(!node) { return false; } - tmp = this.get_node(node); - dpc = []; - dpc.push(node); - dpc = dpc.concat(tmp.children_d); - this.trigger('model', { "nodes" : dpc, "parent" : par.id }); - - par.children_d = par.children_d.concat(dpc); - for(i = 0, j = par.parents.length; i < j; i++) { - this._model.data[par.parents[i]].children_d = this._model.data[par.parents[i]].children_d.concat(dpc); - } - node = tmp; - tmp = []; - for(i = 0, j = par.children.length; i < j; i++) { - tmp[i >= pos ? i+1 : i] = par.children[i]; - } - tmp[pos] = node.id; - par.children = tmp; - - this.redraw_node(par, true); - if(callback) { callback.call(this, this.get_node(node)); } - /** - * triggered when a node is created - * @event - * @name create_node.jstree - * @param {Object} node - * @param {String} parent the parent's ID - * @param {Number} position the position of the new node among the parent's children - */ - this.trigger('create_node', { "node" : this.get_node(node), "parent" : par.id, "position" : pos }); - return node.id; - }, - /** - * set the text value of a node - * @name rename_node(obj, val) - * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name - * @param {String} val the new text value - * @return {Boolean} - * @trigger rename_node.jstree - */ - rename_node : function (obj, val) { - var t1, t2, old; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.rename_node(obj[t1], val); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - old = obj.text; - if(!this.check("rename_node", obj, this.get_parent(obj), val)) { - this.settings.core.error.call(this, this._data.core.last_error); - return false; - } - this.set_text(obj, val); // .apply(this, Array.prototype.slice.call(arguments)) - /** - * triggered when a node is renamed - * @event - * @name rename_node.jstree - * @param {Object} node - * @param {String} text the new value - * @param {String} old the old value - */ - this.trigger('rename_node', { "node" : obj, "text" : val, "old" : old }); - return true; - }, - /** - * remove a node - * @name delete_node(obj) - * @param {mixed} obj the node, you can pass an array to delete multiple nodes - * @return {Boolean} - * @trigger delete_node.jstree, changed.jstree - */ - delete_node : function (obj) { - var t1, t2, par, pos, tmp, i, j, k, l, c, top, lft; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.delete_node(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - par = this.get_node(obj.parent); - pos = $.inArray(obj.id, par.children); - c = false; - if(!this.check("delete_node", obj, par, pos)) { - this.settings.core.error.call(this, this._data.core.last_error); - return false; - } - if(pos !== -1) { - par.children = $.vakata.array_remove(par.children, pos); - } - tmp = obj.children_d.concat([]); - tmp.push(obj.id); - for(k = 0, l = tmp.length; k < l; k++) { - for(i = 0, j = obj.parents.length; i < j; i++) { - pos = $.inArray(tmp[k], this._model.data[obj.parents[i]].children_d); - if(pos !== -1) { - this._model.data[obj.parents[i]].children_d = $.vakata.array_remove(this._model.data[obj.parents[i]].children_d, pos); - } - } - if(this._model.data[tmp[k]].state.selected) { - c = true; - pos = $.inArray(tmp[k], this._data.core.selected); - if(pos !== -1) { - this._data.core.selected = $.vakata.array_remove(this._data.core.selected, pos); - } - } - } - /** - * triggered when a node is deleted - * @event - * @name delete_node.jstree - * @param {Object} node - * @param {String} parent the parent's ID - */ - this.trigger('delete_node', { "node" : obj, "parent" : par.id }); - if(c) { - this.trigger('changed', { 'action' : 'delete_node', 'node' : obj, 'selected' : this._data.core.selected, 'parent' : par.id }); - } - for(k = 0, l = tmp.length; k < l; k++) { - delete this._model.data[tmp[k]]; - } - if($.inArray(this._data.core.focused, tmp) !== -1) { - this._data.core.focused = null; - top = this.element[0].scrollTop; - lft = this.element[0].scrollLeft; - if(par.id === $.jstree.root) { - this.get_node(this._model.data[$.jstree.root].children[0], true).children('.jstree-anchor').focus(); - } - else { - this.get_node(par, true).children('.jstree-anchor').focus(); - } - this.element[0].scrollTop = top; - this.element[0].scrollLeft = lft; - } - this.redraw_node(par, true); - return true; - }, - /** - * check if an operation is premitted on the tree. Used internally. - * @private - * @name check(chk, obj, par, pos) - * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node" - * @param {mixed} obj the node - * @param {mixed} par the parent - * @param {mixed} pos the position to insert at, or if "rename_node" - the new name - * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node - * @return {Boolean} - */ - check : function (chk, obj, par, pos, more) { - obj = obj && obj.id ? obj : this.get_node(obj); - par = par && par.id ? par : this.get_node(par); - var tmp = chk.match(/^move_node|copy_node|create_node$/i) ? par : obj, - chc = this.settings.core.check_callback; - if(chk === "move_node" || chk === "copy_node") { - if((!more || !more.is_multi) && (obj.id === par.id || $.inArray(obj.id, par.children) === pos || $.inArray(par.id, obj.children_d) !== -1)) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_01', 'reason' : 'Moving parent inside child', 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - return false; - } - } - if(tmp && tmp.data) { tmp = tmp.data; } - if(tmp && tmp.functions && (tmp.functions[chk] === false || tmp.functions[chk] === true)) { - if(tmp.functions[chk] === false) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_02', 'reason' : 'Node data prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - } - return tmp.functions[chk]; - } - if(chc === false || ($.isFunction(chc) && chc.call(this, chk, obj, par, pos, more) === false) || (chc && chc[chk] === false)) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_03', 'reason' : 'User config for core.check_callback prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - return false; - } - return true; - }, - /** - * get the last error - * @name last_error() - * @return {Object} - */ - last_error : function () { - return this._data.core.last_error; - }, - /** - * move a node to a new parent - * @name move_node(obj, par [, pos, callback, is_loaded]) - * @param {mixed} obj the node to move, pass an array to move multiple nodes - * @param {mixed} par the new parent - * @param {mixed} pos the position to insert at (besides integer values, "first" and "last" are supported, as well as "before" and "after"), defaults to integer `0` - * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position - * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded - * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn - * @param {Boolean} instance internal parameter indicating if the node comes from another instance - * @trigger move_node.jstree - */ - move_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) { - var t1, t2, old_par, old_pos, new_par, old_ins, is_multi, dpc, tmp, i, j, k, l, p; - - par = this.get_node(par); - pos = pos === undefined ? 0 : pos; - if(!par) { return false; } - if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { - return this.load_node(par, function () { this.move_node(obj, par, pos, callback, true, false, origin); }); - } - - if($.isArray(obj)) { - if(obj.length === 1) { - obj = obj[0]; - } - else { - //obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - if((tmp = this.move_node(obj[t1], par, pos, callback, is_loaded, false, origin))) { - par = tmp; - pos = "after"; - } - } - this.redraw(); - return true; - } - } - obj = obj && obj.id ? obj : this.get_node(obj); - - if(!obj || obj.id === $.jstree.root) { return false; } - - old_par = (obj.parent || $.jstree.root).toString(); - new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent); - old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id)); - is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id); - old_pos = old_ins && old_ins._id && old_par && old_ins._model.data[old_par] && old_ins._model.data[old_par].children ? $.inArray(obj.id, old_ins._model.data[old_par].children) : -1; - if(old_ins && old_ins._id) { - obj = old_ins._model.data[obj.id]; - } - - if(is_multi) { - if((tmp = this.copy_node(obj, par, pos, callback, is_loaded, false, origin))) { - if(old_ins) { old_ins.delete_node(obj); } - return tmp; - } - return false; - } - //var m = this._model.data; - if(par.id === $.jstree.root) { - if(pos === "before") { pos = "first"; } - if(pos === "after") { pos = "last"; } - } - switch(pos) { - case "before": - pos = $.inArray(par.id, new_par.children); - break; - case "after" : - pos = $.inArray(par.id, new_par.children) + 1; - break; - case "inside": - case "first": - pos = 0; - break; - case "last": - pos = new_par.children.length; - break; - default: - if(!pos) { pos = 0; } - break; - } - if(pos > new_par.children.length) { pos = new_par.children.length; } - if(!this.check("move_node", obj, new_par, pos, { 'core' : true, 'origin' : origin, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) { - this.settings.core.error.call(this, this._data.core.last_error); - return false; - } - if(obj.parent === new_par.id) { - dpc = new_par.children.concat(); - tmp = $.inArray(obj.id, dpc); - if(tmp !== -1) { - dpc = $.vakata.array_remove(dpc, tmp); - if(pos > tmp) { pos--; } - } - tmp = []; - for(i = 0, j = dpc.length; i < j; i++) { - tmp[i >= pos ? i+1 : i] = dpc[i]; - } - tmp[pos] = obj.id; - new_par.children = tmp; - this._node_changed(new_par.id); - this.redraw(new_par.id === $.jstree.root); - } - else { - // clean old parent and up - tmp = obj.children_d.concat(); - tmp.push(obj.id); - for(i = 0, j = obj.parents.length; i < j; i++) { - dpc = []; - p = old_ins._model.data[obj.parents[i]].children_d; - for(k = 0, l = p.length; k < l; k++) { - if($.inArray(p[k], tmp) === -1) { - dpc.push(p[k]); - } - } - old_ins._model.data[obj.parents[i]].children_d = dpc; - } - old_ins._model.data[old_par].children = $.vakata.array_remove_item(old_ins._model.data[old_par].children, obj.id); - - // insert into new parent and up - for(i = 0, j = new_par.parents.length; i < j; i++) { - this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(tmp); - } - dpc = []; - for(i = 0, j = new_par.children.length; i < j; i++) { - dpc[i >= pos ? i+1 : i] = new_par.children[i]; - } - dpc[pos] = obj.id; - new_par.children = dpc; - new_par.children_d.push(obj.id); - new_par.children_d = new_par.children_d.concat(obj.children_d); - - // update object - obj.parent = new_par.id; - tmp = new_par.parents.concat(); - tmp.unshift(new_par.id); - p = obj.parents.length; - obj.parents = tmp; - - // update object children - tmp = tmp.concat(); - for(i = 0, j = obj.children_d.length; i < j; i++) { - this._model.data[obj.children_d[i]].parents = this._model.data[obj.children_d[i]].parents.slice(0,p*-1); - Array.prototype.push.apply(this._model.data[obj.children_d[i]].parents, tmp); - } - - if(old_par === $.jstree.root || new_par.id === $.jstree.root) { - this._model.force_full_redraw = true; - } - if(!this._model.force_full_redraw) { - this._node_changed(old_par); - this._node_changed(new_par.id); - } - if(!skip_redraw) { - this.redraw(); - } - } - if(callback) { callback.call(this, obj, new_par, pos); } - /** - * triggered when a node is moved - * @event - * @name move_node.jstree - * @param {Object} node - * @param {String} parent the parent's ID - * @param {Number} position the position of the node among the parent's children - * @param {String} old_parent the old parent of the node - * @param {Number} old_position the old position of the node - * @param {Boolean} is_multi do the node and new parent belong to different instances - * @param {jsTree} old_instance the instance the node came from - * @param {jsTree} new_instance the instance of the new parent - */ - this.trigger('move_node', { "node" : obj, "parent" : new_par.id, "position" : pos, "old_parent" : old_par, "old_position" : old_pos, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id), 'old_instance' : old_ins, 'new_instance' : this }); - return obj.id; - }, - /** - * copy a node to a new parent - * @name copy_node(obj, par [, pos, callback, is_loaded]) - * @param {mixed} obj the node to copy, pass an array to copy multiple nodes - * @param {mixed} par the new parent - * @param {mixed} pos the position to insert at (besides integer values, "first" and "last" are supported, as well as "before" and "after"), defaults to integer `0` - * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position - * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded - * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn - * @param {Boolean} instance internal parameter indicating if the node comes from another instance - * @trigger model.jstree copy_node.jstree - */ - copy_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) { - var t1, t2, dpc, tmp, i, j, node, old_par, new_par, old_ins, is_multi; - - par = this.get_node(par); - pos = pos === undefined ? 0 : pos; - if(!par) { return false; } - if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { - return this.load_node(par, function () { this.copy_node(obj, par, pos, callback, true, false, origin); }); - } - - if($.isArray(obj)) { - if(obj.length === 1) { - obj = obj[0]; - } - else { - //obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - if((tmp = this.copy_node(obj[t1], par, pos, callback, is_loaded, true, origin))) { - par = tmp; - pos = "after"; - } - } - this.redraw(); - return true; - } - } - obj = obj && obj.id ? obj : this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - - old_par = (obj.parent || $.jstree.root).toString(); - new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent); - old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id)); - is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id); - - if(old_ins && old_ins._id) { - obj = old_ins._model.data[obj.id]; - } - - if(par.id === $.jstree.root) { - if(pos === "before") { pos = "first"; } - if(pos === "after") { pos = "last"; } - } - switch(pos) { - case "before": - pos = $.inArray(par.id, new_par.children); - break; - case "after" : - pos = $.inArray(par.id, new_par.children) + 1; - break; - case "inside": - case "first": - pos = 0; - break; - case "last": - pos = new_par.children.length; - break; - default: - if(!pos) { pos = 0; } - break; - } - if(pos > new_par.children.length) { pos = new_par.children.length; } - if(!this.check("copy_node", obj, new_par, pos, { 'core' : true, 'origin' : origin, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) { - this.settings.core.error.call(this, this._data.core.last_error); - return false; - } - node = old_ins ? old_ins.get_json(obj, { no_id : true, no_data : true, no_state : true }) : obj; - if(!node) { return false; } - if(node.id === true) { delete node.id; } - node = this._parse_model_from_json(node, new_par.id, new_par.parents.concat()); - if(!node) { return false; } - tmp = this.get_node(node); - if(obj && obj.state && obj.state.loaded === false) { tmp.state.loaded = false; } - dpc = []; - dpc.push(node); - dpc = dpc.concat(tmp.children_d); - this.trigger('model', { "nodes" : dpc, "parent" : new_par.id }); - - // insert into new parent and up - for(i = 0, j = new_par.parents.length; i < j; i++) { - this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(dpc); - } - dpc = []; - for(i = 0, j = new_par.children.length; i < j; i++) { - dpc[i >= pos ? i+1 : i] = new_par.children[i]; - } - dpc[pos] = tmp.id; - new_par.children = dpc; - new_par.children_d.push(tmp.id); - new_par.children_d = new_par.children_d.concat(tmp.children_d); - - if(new_par.id === $.jstree.root) { - this._model.force_full_redraw = true; - } - if(!this._model.force_full_redraw) { - this._node_changed(new_par.id); - } - if(!skip_redraw) { - this.redraw(new_par.id === $.jstree.root); - } - if(callback) { callback.call(this, tmp, new_par, pos); } - /** - * triggered when a node is copied - * @event - * @name copy_node.jstree - * @param {Object} node the copied node - * @param {Object} original the original node - * @param {String} parent the parent's ID - * @param {Number} position the position of the node among the parent's children - * @param {String} old_parent the old parent of the node - * @param {Number} old_position the position of the original node - * @param {Boolean} is_multi do the node and new parent belong to different instances - * @param {jsTree} old_instance the instance the node came from - * @param {jsTree} new_instance the instance of the new parent - */ - this.trigger('copy_node', { "node" : tmp, "original" : obj, "parent" : new_par.id, "position" : pos, "old_parent" : old_par, "old_position" : old_ins && old_ins._id && old_par && old_ins._model.data[old_par] && old_ins._model.data[old_par].children ? $.inArray(obj.id, old_ins._model.data[old_par].children) : -1,'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id), 'old_instance' : old_ins, 'new_instance' : this }); - return tmp.id; - }, - /** - * cut a node (a later call to `paste(obj)` would move the node) - * @name cut(obj) - * @param {mixed} obj multiple objects can be passed using an array - * @trigger cut.jstree - */ - cut : function (obj) { - if(!obj) { obj = this._data.core.selected.concat(); } - if(!$.isArray(obj)) { obj = [obj]; } - if(!obj.length) { return false; } - var tmp = [], o, t1, t2; - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - o = this.get_node(obj[t1]); - if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); } - } - if(!tmp.length) { return false; } - ccp_node = tmp; - ccp_inst = this; - ccp_mode = 'move_node'; - /** - * triggered when nodes are added to the buffer for moving - * @event - * @name cut.jstree - * @param {Array} node - */ - this.trigger('cut', { "node" : obj }); - }, - /** - * copy a node (a later call to `paste(obj)` would copy the node) - * @name copy(obj) - * @param {mixed} obj multiple objects can be passed using an array - * @trigger copy.jstree - */ - copy : function (obj) { - if(!obj) { obj = this._data.core.selected.concat(); } - if(!$.isArray(obj)) { obj = [obj]; } - if(!obj.length) { return false; } - var tmp = [], o, t1, t2; - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - o = this.get_node(obj[t1]); - if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); } - } - if(!tmp.length) { return false; } - ccp_node = tmp; - ccp_inst = this; - ccp_mode = 'copy_node'; - /** - * triggered when nodes are added to the buffer for copying - * @event - * @name copy.jstree - * @param {Array} node - */ - this.trigger('copy', { "node" : obj }); - }, - /** - * get the current buffer (any nodes that are waiting for a paste operation) - * @name get_buffer() - * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance) - */ - get_buffer : function () { - return { 'mode' : ccp_mode, 'node' : ccp_node, 'inst' : ccp_inst }; - }, - /** - * check if there is something in the buffer to paste - * @name can_paste() - * @return {Boolean} - */ - can_paste : function () { - return ccp_mode !== false && ccp_node !== false; // && ccp_inst._model.data[ccp_node]; - }, - /** - * copy or move the previously cut or copied nodes to a new parent - * @name paste(obj [, pos]) - * @param {mixed} obj the new parent - * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0` - * @trigger paste.jstree - */ - paste : function (obj, pos) { - obj = this.get_node(obj); - if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; } - if(this[ccp_mode](ccp_node, obj, pos, false, false, false, ccp_inst)) { - /** - * triggered when paste is invoked - * @event - * @name paste.jstree - * @param {String} parent the ID of the receiving node - * @param {Array} node the nodes in the buffer - * @param {String} mode the performed operation - "copy_node" or "move_node" - */ - this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode }); - } - ccp_node = false; - ccp_mode = false; - ccp_inst = false; - }, - /** - * clear the buffer of previously copied or cut nodes - * @name clear_buffer() - * @trigger clear_buffer.jstree - */ - clear_buffer : function () { - ccp_node = false; - ccp_mode = false; - ccp_inst = false; - /** - * triggered when the copy / cut buffer is cleared - * @event - * @name clear_buffer.jstree - */ - this.trigger('clear_buffer'); - }, - /** - * put a node in edit mode (input field to rename the node) - * @name edit(obj [, default_text, callback]) - * @param {mixed} obj - * @param {String} default_text the text to populate the input with (if omitted or set to a non-string value the node's text value is used) - * @param {Function} callback a function to be called once the text box is blurred, it is called in the instance's scope and receives the node, a status parameter (true if the rename is successful, false otherwise) and a boolean indicating if the user cancelled the edit. You can access the node's title using .text - */ - edit : function (obj, default_text, callback) { - var rtl, w, a, s, t, h1, h2, fn, tmp, cancel = false; - obj = this.get_node(obj); - if(!obj) { return false; } - if(this.settings.core.check_callback === false) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_07', 'reason' : 'Could not edit node because of check_callback' }; - this.settings.core.error.call(this, this._data.core.last_error); - return false; - } - tmp = obj; - default_text = typeof default_text === 'string' ? default_text : obj.text; - this.set_text(obj, ""); - obj = this._open_to(obj); - tmp.text = default_text; - - rtl = this._data.core.rtl; - w = this.element.width(); - this._data.core.focused = tmp.id; - a = obj.children('.jstree-anchor').focus(); - s = $(''); - /*! - oi = obj.children("i:visible"), - ai = a.children("i:visible"), - w1 = oi.width() * oi.length, - w2 = ai.width() * ai.length, - */ - t = default_text; - h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"); - h2 = $("<"+"input />", { - "value" : t, - "class" : "jstree-rename-input", - // "size" : t.length, - "css" : { - "padding" : "0", - "border" : "1px solid silver", - "box-sizing" : "border-box", - "display" : "inline-block", - "height" : (this._data.core.li_height) + "px", - "lineHeight" : (this._data.core.li_height) + "px", - "width" : "150px" // will be set a bit further down - }, - "blur" : $.proxy(function (e) { - e.stopImmediatePropagation(); - e.preventDefault(); - var i = s.children(".jstree-rename-input"), - v = i.val(), - f = this.settings.core.force_text, - nv; - if(v === "") { v = t; } - h1.remove(); - s.replaceWith(a); - s.remove(); - t = f ? t : $('
    ').append($.parseHTML(t)).html(); - this.set_text(obj, t); - nv = !!this.rename_node(obj, f ? $('
    ').text(v).text() : $('
    ').append($.parseHTML(v)).html()); - if(!nv) { - this.set_text(obj, t); // move this up? and fix #483 - } - this._data.core.focused = tmp.id; - setTimeout($.proxy(function () { - var node = this.get_node(tmp.id, true); - if(node.length) { - this._data.core.focused = tmp.id; - node.children('.jstree-anchor').focus(); - } - }, this), 0); - if(callback) { - callback.call(this, tmp, nv, cancel); - } - }, this), - "keydown" : function (e) { - var key = e.which; - if(key === 27) { - cancel = true; - this.value = t; - } - if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) { - e.stopImmediatePropagation(); - } - if(key === 27 || key === 13) { - e.preventDefault(); - this.blur(); - } - }, - "click" : function (e) { e.stopImmediatePropagation(); }, - "mousedown" : function (e) { e.stopImmediatePropagation(); }, - "keyup" : function (e) { - h2.width(Math.min(h1.text("pW" + this.value).width(),w)); - }, - "keypress" : function(e) { - if(e.which === 13) { return false; } - } - }); - fn = { - fontFamily : a.css('fontFamily') || '', - fontSize : a.css('fontSize') || '', - fontWeight : a.css('fontWeight') || '', - fontStyle : a.css('fontStyle') || '', - fontStretch : a.css('fontStretch') || '', - fontVariant : a.css('fontVariant') || '', - letterSpacing : a.css('letterSpacing') || '', - wordSpacing : a.css('wordSpacing') || '' - }; - s.attr('class', a.attr('class')).append(a.contents().clone()).append(h2); - a.replaceWith(s); - h1.css(fn); - h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select(); - }, - - - /** - * changes the theme - * @name set_theme(theme_name [, theme_url]) - * @param {String} theme_name the name of the new theme to apply - * @param {mixed} theme_url the location of the CSS file for this theme. Omit or set to `false` if you manually included the file. Set to `true` to autoload from the `core.themes.dir` directory. - * @trigger set_theme.jstree - */ - set_theme : function (theme_name, theme_url) { - if(!theme_name) { return false; } - if(theme_url === true) { - var dir = this.settings.core.themes.dir; - if(!dir) { dir = $.jstree.path + '/themes'; } - theme_url = dir + '/' + theme_name + '/style.css'; - } - if(theme_url && $.inArray(theme_url, themes_loaded) === -1) { - $('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />'); - themes_loaded.push(theme_url); - } - if(this._data.core.themes.name) { - this.element.removeClass('jstree-' + this._data.core.themes.name); - } - this._data.core.themes.name = theme_name; - this.element.addClass('jstree-' + theme_name); - this.element[this.settings.core.themes.responsive ? 'addClass' : 'removeClass' ]('jstree-' + theme_name + '-responsive'); - /** - * triggered when a theme is set - * @event - * @name set_theme.jstree - * @param {String} theme the new theme - */ - this.trigger('set_theme', { 'theme' : theme_name }); - }, - /** - * gets the name of the currently applied theme name - * @name get_theme() - * @return {String} - */ - get_theme : function () { return this._data.core.themes.name; }, - /** - * changes the theme variant (if the theme has variants) - * @name set_theme_variant(variant_name) - * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed) - */ - set_theme_variant : function (variant_name) { - if(this._data.core.themes.variant) { - this.element.removeClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant); - } - this._data.core.themes.variant = variant_name; - if(variant_name) { - this.element.addClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant); - } - }, - /** - * gets the name of the currently applied theme variant - * @name get_theme() - * @return {String} - */ - get_theme_variant : function () { return this._data.core.themes.variant; }, - /** - * shows a striped background on the container (if the theme supports it) - * @name show_stripes() - */ - show_stripes : function () { this._data.core.themes.stripes = true; this.get_container_ul().addClass("jstree-striped"); }, - /** - * hides the striped background on the container - * @name hide_stripes() - */ - hide_stripes : function () { this._data.core.themes.stripes = false; this.get_container_ul().removeClass("jstree-striped"); }, - /** - * toggles the striped background on the container - * @name toggle_stripes() - */ - toggle_stripes : function () { if(this._data.core.themes.stripes) { this.hide_stripes(); } else { this.show_stripes(); } }, - /** - * shows the connecting dots (if the theme supports it) - * @name show_dots() - */ - show_dots : function () { this._data.core.themes.dots = true; this.get_container_ul().removeClass("jstree-no-dots"); }, - /** - * hides the connecting dots - * @name hide_dots() - */ - hide_dots : function () { this._data.core.themes.dots = false; this.get_container_ul().addClass("jstree-no-dots"); }, - /** - * toggles the connecting dots - * @name toggle_dots() - */ - toggle_dots : function () { if(this._data.core.themes.dots) { this.hide_dots(); } else { this.show_dots(); } }, - /** - * show the node icons - * @name show_icons() - */ - show_icons : function () { this._data.core.themes.icons = true; this.get_container_ul().removeClass("jstree-no-icons"); }, - /** - * hide the node icons - * @name hide_icons() - */ - hide_icons : function () { this._data.core.themes.icons = false; this.get_container_ul().addClass("jstree-no-icons"); }, - /** - * toggle the node icons - * @name toggle_icons() - */ - toggle_icons : function () { if(this._data.core.themes.icons) { this.hide_icons(); } else { this.show_icons(); } }, - /** - * set the node icon for a node - * @name set_icon(obj, icon) - * @param {mixed} obj - * @param {String} icon the new icon - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class - */ - set_icon : function (obj, icon) { - var t1, t2, dom, old; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.set_icon(obj[t1], icon); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - old = obj.icon; - obj.icon = icon === true || icon === null || icon === undefined || icon === '' ? true : icon; - dom = this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon"); - if(icon === false) { - this.hide_icon(obj); - } - else if(icon === true || icon === null || icon === undefined || icon === '') { - dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel"); - if(old === false) { this.show_icon(obj); } - } - else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) { - dom.removeClass(old).css("background",""); - dom.addClass(icon + ' jstree-themeicon-custom').attr("rel",icon); - if(old === false) { this.show_icon(obj); } - } - else { - dom.removeClass(old).css("background",""); - dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon); - if(old === false) { this.show_icon(obj); } - } - return true; - }, - /** - * get the node icon for a node - * @name get_icon(obj) - * @param {mixed} obj - * @return {String} - */ - get_icon : function (obj) { - obj = this.get_node(obj); - return (!obj || obj.id === $.jstree.root) ? false : obj.icon; - }, - /** - * hide the icon on an individual node - * @name hide_icon(obj) - * @param {mixed} obj - */ - hide_icon : function (obj) { - var t1, t2; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.hide_icon(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj === $.jstree.root) { return false; } - obj.icon = false; - this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden'); - return true; - }, - /** - * show the icon on an individual node - * @name show_icon(obj) - * @param {mixed} obj - */ - show_icon : function (obj) { - var t1, t2, dom; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.show_icon(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj === $.jstree.root) { return false; } - dom = this.get_node(obj, true); - obj.icon = dom.length ? dom.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true; - if(!obj.icon) { obj.icon = true; } - dom.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden'); - return true; - } - }; - - // helpers - $.vakata = {}; - // collect attributes - $.vakata.attributes = function(node, with_values) { - node = $(node)[0]; - var attr = with_values ? {} : []; - if(node && node.attributes) { - $.each(node.attributes, function (i, v) { - if($.inArray(v.name.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; } - if(v.value !== null && $.trim(v.value) !== '') { - if(with_values) { attr[v.name] = v.value; } - else { attr.push(v.name); } - } - }); - } - return attr; - }; - $.vakata.array_unique = function(array) { - var a = [], i, j, l, o = {}; - for(i = 0, l = array.length; i < l; i++) { - if(o[array[i]] === undefined) { - a.push(array[i]); - o[array[i]] = true; - } - } - return a; - }; - // remove item from array - $.vakata.array_remove = function(array, from, to) { - var rest = array.slice((to || from) + 1 || array.length); - array.length = from < 0 ? array.length + from : from; - array.push.apply(array, rest); - return array; - }; - // remove item from array - $.vakata.array_remove_item = function(array, item) { - var tmp = $.inArray(item, array); - return tmp !== -1 ? $.vakata.array_remove(array, tmp) : array; - }; - - -/** - * ### Changed plugin - * - * This plugin adds more information to the `changed.jstree` event. The new data is contained in the `changed` event data property, and contains a lists of `selected` and `deselected` nodes. - */ - - $.jstree.plugins.changed = function (options, parent) { - var last = []; - this.trigger = function (ev, data) { - var i, j; - if(!data) { - data = {}; - } - if(ev.replace('.jstree','') === 'changed') { - data.changed = { selected : [], deselected : [] }; - var tmp = {}; - for(i = 0, j = last.length; i < j; i++) { - tmp[last[i]] = 1; - } - for(i = 0, j = data.selected.length; i < j; i++) { - if(!tmp[data.selected[i]]) { - data.changed.selected.push(data.selected[i]); - } - else { - tmp[data.selected[i]] = 2; - } - } - for(i = 0, j = last.length; i < j; i++) { - if(tmp[last[i]] === 1) { - data.changed.deselected.push(last[i]); - } - } - last = data.selected.slice(); - } - /** - * triggered when selection changes (the "changed" plugin enhances the original event with more data) - * @event - * @name changed.jstree - * @param {Object} node - * @param {Object} action the action that caused the selection to change - * @param {Array} selected the current selection - * @param {Object} changed an object containing two properties `selected` and `deselected` - both arrays of node IDs, which were selected or deselected since the last changed event - * @param {Object} event the event (if any) that triggered this changed event - * @plugin changed - */ - parent.trigger.call(this, ev, data); - }; - this.refresh = function (skip_loading, forget_state) { - last = []; - return parent.refresh.apply(this, arguments); - }; - }; - -/** - * ### Checkbox plugin - * - * This plugin renders checkbox icons in front of each node, making multiple selection much easier. - * It also supports tri-state behavior, meaning that if a node has a few of its children checked it will be rendered as undetermined, and state will be propagated up. - */ - - var _i = document.createElement('I'); - _i.className = 'jstree-icon jstree-checkbox'; - _i.setAttribute('role', 'presentation'); - /** - * stores all defaults for the checkbox plugin - * @name $.jstree.defaults.checkbox - * @plugin checkbox - */ - $.jstree.defaults.checkbox = { - /** - * a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`. - * @name $.jstree.defaults.checkbox.visible - * @plugin checkbox - */ - visible : true, - /** - * a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`. - * @name $.jstree.defaults.checkbox.three_state - * @plugin checkbox - */ - three_state : true, - /** - * a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`. - * @name $.jstree.defaults.checkbox.whole_node - * @plugin checkbox - */ - whole_node : true, - /** - * a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`. - * @name $.jstree.defaults.checkbox.keep_selected_style - * @plugin checkbox - */ - keep_selected_style : true, - /** - * This setting controls how cascading and undetermined nodes are applied. - * If 'up' is in the string - cascading up is enabled, if 'down' is in the string - cascading down is enabled, if 'undetermined' is in the string - undetermined nodes will be used. - * If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''. - * @name $.jstree.defaults.checkbox.cascade - * @plugin checkbox - */ - cascade : '', - /** - * This setting controls if checkbox are bound to the general tree selection or to an internal array maintained by the checkbox plugin. Defaults to `true`, only set to `false` if you know exactly what you are doing. - * @name $.jstree.defaults.checkbox.tie_selection - * @plugin checkbox - */ - tie_selection : true - }; - $.jstree.plugins.checkbox = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - this._data.checkbox.uto = false; - this._data.checkbox.selected = []; - if(this.settings.checkbox.three_state) { - this.settings.checkbox.cascade = 'up+down+undetermined'; - } - this.element - .on("init.jstree", $.proxy(function () { - this._data.checkbox.visible = this.settings.checkbox.visible; - if(!this.settings.checkbox.keep_selected_style) { - this.element.addClass('jstree-checkbox-no-clicked'); - } - if(this.settings.checkbox.tie_selection) { - this.element.addClass('jstree-checkbox-selection'); - } - }, this)) - .on("loading.jstree", $.proxy(function () { - this[ this._data.checkbox.visible ? 'show_checkboxes' : 'hide_checkboxes' ](); - }, this)); - if(this.settings.checkbox.cascade.indexOf('undetermined') !== -1) { - this.element - .on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree', $.proxy(function () { - // only if undetermined is in setting - if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); } - this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50); - }, this)); - } - if(!this.settings.checkbox.tie_selection) { - this.element - .on('model.jstree', $.proxy(function (e, data) { - var m = this._model.data, - p = m[data.parent], - dpc = data.nodes, - i, j; - for(i = 0, j = dpc.length; i < j; i++) { - m[dpc[i]].state.checked = m[dpc[i]].state.checked || (m[dpc[i]].original && m[dpc[i]].original.state && m[dpc[i]].original.state.checked); - if(m[dpc[i]].state.checked) { - this._data.checkbox.selected.push(dpc[i]); - } - } - }, this)); - } - if(this.settings.checkbox.cascade.indexOf('up') !== -1 || this.settings.checkbox.cascade.indexOf('down') !== -1) { - this.element - .on('model.jstree', $.proxy(function (e, data) { - var m = this._model.data, - p = m[data.parent], - dpc = data.nodes, - chd = [], - c, i, j, k, l, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection; - - if(s.indexOf('down') !== -1) { - // apply down - if(p.state[ t ? 'selected' : 'checked' ]) { - for(i = 0, j = dpc.length; i < j; i++) { - m[dpc[i]].state[ t ? 'selected' : 'checked' ] = true; - } - this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(dpc); - } - else { - for(i = 0, j = dpc.length; i < j; i++) { - if(m[dpc[i]].state[ t ? 'selected' : 'checked' ]) { - for(k = 0, l = m[dpc[i]].children_d.length; k < l; k++) { - m[m[dpc[i]].children_d[k]].state[ t ? 'selected' : 'checked' ] = true; - } - this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(m[dpc[i]].children_d); - } - } - } - } - - if(s.indexOf('up') !== -1) { - // apply up - for(i = 0, j = p.children_d.length; i < j; i++) { - if(!m[p.children_d[i]].children.length) { - chd.push(m[p.children_d[i]].parent); - } - } - chd = $.vakata.array_unique(chd); - for(k = 0, l = chd.length; k < l; k++) { - p = m[chd[k]]; - while(p && p.id !== $.jstree.root) { - c = 0; - for(i = 0, j = p.children.length; i < j; i++) { - c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; - } - if(c === j) { - p.state[ t ? 'selected' : 'checked' ] = true; - this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); - tmp = this.get_node(p, true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', true).children('.jstree-anchor').addClass( t ? 'jstree-clicked' : 'jstree-checked'); - } - } - else { - break; - } - p = this.get_node(p.parent); - } - } - } - - this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected); - }, this)) - .on(this.settings.checkbox.tie_selection ? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e, data) { - var obj = data.node, - m = this._model.data, - par = this.get_node(obj.parent), - dom = this.get_node(obj, true), - i, j, c, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection; - - // apply down - if(s.indexOf('down') !== -1) { - this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d)); - for(i = 0, j = obj.children_d.length; i < j; i++) { - tmp = m[obj.children_d[i]]; - tmp.state[ t ? 'selected' : 'checked' ] = true; - if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { - tmp.original.state.undetermined = false; - } - } - } - - // apply up - if(s.indexOf('up') !== -1) { - while(par && par.id !== $.jstree.root) { - c = 0; - for(i = 0, j = par.children.length; i < j; i++) { - c += m[par.children[i]].state[ t ? 'selected' : 'checked' ]; - } - if(c === j) { - par.state[ t ? 'selected' : 'checked' ] = true; - this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id); - tmp = this.get_node(par, true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); - } - } - else { - break; - } - par = this.get_node(par.parent); - } - } - - // apply down (process .children separately?) - if(s.indexOf('down') !== -1 && dom.length) { - dom.find('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', true); - } - }, this)) - .on(this.settings.checkbox.tie_selection ? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e, data) { - var obj = this.get_node($.jstree.root), - m = this._model.data, - i, j, tmp; - for(i = 0, j = obj.children_d.length; i < j; i++) { - tmp = m[obj.children_d[i]]; - if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { - tmp.original.state.undetermined = false; - } - } - }, this)) - .on(this.settings.checkbox.tie_selection ? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e, data) { - var obj = data.node, - dom = this.get_node(obj, true), - i, j, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection; - if(obj && obj.original && obj.original.state && obj.original.state.undetermined) { - obj.original.state.undetermined = false; - } - - // apply down - if(s.indexOf('down') !== -1) { - for(i = 0, j = obj.children_d.length; i < j; i++) { - tmp = this._model.data[obj.children_d[i]]; - tmp.state[ t ? 'selected' : 'checked' ] = false; - if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { - tmp.original.state.undetermined = false; - } - } - } - - // apply up - if(s.indexOf('up') !== -1) { - for(i = 0, j = obj.parents.length; i < j; i++) { - tmp = this._model.data[obj.parents[i]]; - tmp.state[ t ? 'selected' : 'checked' ] = false; - if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { - tmp.original.state.undetermined = false; - } - tmp = this.get_node(obj.parents[i], true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked'); - } - } - } - tmp = []; - for(i = 0, j = this._data[ t ? 'core' : 'checkbox' ].selected.length; i < j; i++) { - // apply down + apply up - if( - (s.indexOf('down') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.children_d) === -1) && - (s.indexOf('up') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.parents) === -1) - ) { - tmp.push(this._data[ t ? 'core' : 'checkbox' ].selected[i]); - } - } - this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(tmp); - - // apply down (process .children separately?) - if(s.indexOf('down') !== -1 && dom.length) { - dom.find('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', false); - } - }, this)); - } - if(this.settings.checkbox.cascade.indexOf('up') !== -1) { - this.element - .on('delete_node.jstree', $.proxy(function (e, data) { - // apply up (whole handler) - var p = this.get_node(data.parent), - m = this._model.data, - i, j, c, tmp, t = this.settings.checkbox.tie_selection; - while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) { - c = 0; - for(i = 0, j = p.children.length; i < j; i++) { - c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; - } - if(j > 0 && c === j) { - p.state[ t ? 'selected' : 'checked' ] = true; - this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); - tmp = this.get_node(p, true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); - } - } - else { - break; - } - p = this.get_node(p.parent); - } - }, this)) - .on('move_node.jstree', $.proxy(function (e, data) { - // apply up (whole handler) - var is_multi = data.is_multi, - old_par = data.old_parent, - new_par = this.get_node(data.parent), - m = this._model.data, - p, c, i, j, tmp, t = this.settings.checkbox.tie_selection; - if(!is_multi) { - p = this.get_node(old_par); - while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) { - c = 0; - for(i = 0, j = p.children.length; i < j; i++) { - c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; - } - if(j > 0 && c === j) { - p.state[ t ? 'selected' : 'checked' ] = true; - this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); - tmp = this.get_node(p, true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); - } - } - else { - break; - } - p = this.get_node(p.parent); - } - } - p = new_par; - while(p && p.id !== $.jstree.root) { - c = 0; - for(i = 0, j = p.children.length; i < j; i++) { - c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; - } - if(c === j) { - if(!p.state[ t ? 'selected' : 'checked' ]) { - p.state[ t ? 'selected' : 'checked' ] = true; - this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); - tmp = this.get_node(p, true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); - } - } - } - else { - if(p.state[ t ? 'selected' : 'checked' ]) { - p.state[ t ? 'selected' : 'checked' ] = false; - this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_remove_item(this._data[ t ? 'core' : 'checkbox' ].selected, p.id); - tmp = this.get_node(p, true); - if(tmp && tmp.length) { - tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked'); - } - } - else { - break; - } - } - p = this.get_node(p.parent); - } - }, this)); - } - }; - /** - * set the undetermined state where and if necessary. Used internally. - * @private - * @name _undetermined() - * @plugin checkbox - */ - this._undetermined = function () { - if(this.element === null) { return; } - var i, j, k, l, o = {}, m = this._model.data, t = this.settings.checkbox.tie_selection, s = this._data[ t ? 'core' : 'checkbox' ].selected, p = [], tt = this; - for(i = 0, j = s.length; i < j; i++) { - if(m[s[i]] && m[s[i]].parents) { - for(k = 0, l = m[s[i]].parents.length; k < l; k++) { - if(o[m[s[i]].parents[k]] === undefined && m[s[i]].parents[k] !== $.jstree.root) { - o[m[s[i]].parents[k]] = true; - p.push(m[s[i]].parents[k]); - } - } - } - } - // attempt for server side undetermined state - this.element.find('.jstree-closed').not(':has(.jstree-children)') - .each(function () { - var tmp = tt.get_node(this), tmp2; - if(!tmp.state.loaded) { - if(tmp.original && tmp.original.state && tmp.original.state.undetermined && tmp.original.state.undetermined === true) { - if(o[tmp.id] === undefined && tmp.id !== $.jstree.root) { - o[tmp.id] = true; - p.push(tmp.id); - } - for(k = 0, l = tmp.parents.length; k < l; k++) { - if(o[tmp.parents[k]] === undefined && tmp.parents[k] !== $.jstree.root) { - o[tmp.parents[k]] = true; - p.push(tmp.parents[k]); - } - } - } - } - else { - for(i = 0, j = tmp.children_d.length; i < j; i++) { - tmp2 = m[tmp.children_d[i]]; - if(!tmp2.state.loaded && tmp2.original && tmp2.original.state && tmp2.original.state.undetermined && tmp2.original.state.undetermined === true) { - if(o[tmp2.id] === undefined && tmp2.id !== $.jstree.root) { - o[tmp2.id] = true; - p.push(tmp2.id); - } - for(k = 0, l = tmp2.parents.length; k < l; k++) { - if(o[tmp2.parents[k]] === undefined && tmp2.parents[k] !== $.jstree.root) { - o[tmp2.parents[k]] = true; - p.push(tmp2.parents[k]); - } - } - } - } - } - }); - - this.element.find('.jstree-undetermined').removeClass('jstree-undetermined'); - for(i = 0, j = p.length; i < j; i++) { - if(!m[p[i]].state[ t ? 'selected' : 'checked' ]) { - s = this.get_node(p[i], true); - if(s && s.length) { - s.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined'); - } - } - } - }; - this.redraw_node = function(obj, deep, is_callback, force_render) { - obj = parent.redraw_node.apply(this, arguments); - if(obj) { - var i, j, tmp = null, icon = null; - for(i = 0, j = obj.childNodes.length; i < j; i++) { - if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) { - tmp = obj.childNodes[i]; - break; - } - } - if(tmp) { - if(!this.settings.checkbox.tie_selection && this._model.data[obj.id].state.checked) { tmp.className += ' jstree-checked'; } - icon = _i.cloneNode(false); - if(this._model.data[obj.id].state.checkbox_disabled) { icon.className += ' jstree-checkbox-disabled'; } - tmp.insertBefore(icon, tmp.childNodes[0]); - } - } - if(!is_callback && this.settings.checkbox.cascade.indexOf('undetermined') !== -1) { - if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); } - this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50); - } - return obj; - }; - /** - * show the node checkbox icons - * @name show_checkboxes() - * @plugin checkbox - */ - this.show_checkboxes = function () { this._data.core.themes.checkboxes = true; this.get_container_ul().removeClass("jstree-no-checkboxes"); }; - /** - * hide the node checkbox icons - * @name hide_checkboxes() - * @plugin checkbox - */ - this.hide_checkboxes = function () { this._data.core.themes.checkboxes = false; this.get_container_ul().addClass("jstree-no-checkboxes"); }; - /** - * toggle the node icons - * @name toggle_checkboxes() - * @plugin checkbox - */ - this.toggle_checkboxes = function () { if(this._data.core.themes.checkboxes) { this.hide_checkboxes(); } else { this.show_checkboxes(); } }; - /** - * checks if a node is in an undetermined state - * @name is_undetermined(obj) - * @param {mixed} obj - * @return {Boolean} - */ - this.is_undetermined = function (obj) { - obj = this.get_node(obj); - var s = this.settings.checkbox.cascade, i, j, t = this.settings.checkbox.tie_selection, d = this._data[ t ? 'core' : 'checkbox' ].selected, m = this._model.data; - if(!obj || obj.state[ t ? 'selected' : 'checked' ] === true || s.indexOf('undetermined') === -1 || (s.indexOf('down') === -1 && s.indexOf('up') === -1)) { - return false; - } - if(!obj.state.loaded && obj.original.state.undetermined === true) { - return true; - } - for(i = 0, j = obj.children_d.length; i < j; i++) { - if($.inArray(obj.children_d[i], d) !== -1 || (!m[obj.children_d[i]].state.loaded && m[obj.children_d[i]].original.state.undetermined)) { - return true; - } - } - return false; - }; - /** - * disable a node's checkbox - * @name disable_checkbox(obj) - * @param {mixed} obj an array can be used too - * @trigger disable_checkbox.jstree - * @plugin checkbox - */ - this.disable_checkbox = function (obj) { - var t1, t2, dom; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.disable_checkbox(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - dom = this.get_node(obj, true); - if(!obj.state.checkbox_disabled) { - obj.state.checkbox_disabled = true; - if(dom && dom.length) { - dom.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-checkbox-disabled'); - } - /** - * triggered when an node's checkbox is disabled - * @event - * @name disable_checkbox.jstree - * @param {Object} node - * @plugin checkbox - */ - this.trigger('disable_checkbox', { 'node' : obj }); - } - }; - /** - * enable a node's checkbox - * @name disable_checkbox(obj) - * @param {mixed} obj an array can be used too - * @trigger enable_checkbox.jstree - * @plugin checkbox - */ - this.enable_checkbox = function (obj) { - var t1, t2, dom; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.enable_checkbox(obj[t1]); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - dom = this.get_node(obj, true); - if(obj.state.checkbox_disabled) { - obj.state.checkbox_disabled = false; - if(dom && dom.length) { - dom.children('.jstree-anchor').children('.jstree-checkbox').removeClass('jstree-checkbox-disabled'); - } - /** - * triggered when an node's checkbox is enabled - * @event - * @name enable_checkbox.jstree - * @param {Object} node - * @plugin checkbox - */ - this.trigger('enable_checkbox', { 'node' : obj }); - } - }; - - this.activate_node = function (obj, e) { - if($(e.target).hasClass('jstree-checkbox-disabled')) { - return false; - } - if(this.settings.checkbox.tie_selection && (this.settings.checkbox.whole_node || $(e.target).hasClass('jstree-checkbox'))) { - e.ctrlKey = true; - } - if(this.settings.checkbox.tie_selection || (!this.settings.checkbox.whole_node && !$(e.target).hasClass('jstree-checkbox'))) { - return parent.activate_node.call(this, obj, e); - } - if(this.is_disabled(obj)) { - return false; - } - if(this.is_checked(obj)) { - this.uncheck_node(obj, e); - } - else { - this.check_node(obj, e); - } - this.trigger('activate_node', { 'node' : this.get_node(obj) }); - }; - - /** - * check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally) - * @name check_node(obj) - * @param {mixed} obj an array can be used to check multiple nodes - * @trigger check_node.jstree - * @plugin checkbox - */ - this.check_node = function (obj, e) { - if(this.settings.checkbox.tie_selection) { return this.select_node(obj, false, true, e); } - var dom, t1, t2, th; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.check_node(obj[t1], e); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - dom = this.get_node(obj, true); - if(!obj.state.checked) { - obj.state.checked = true; - this._data.checkbox.selected.push(obj.id); - if(dom && dom.length) { - dom.children('.jstree-anchor').addClass('jstree-checked'); - } - /** - * triggered when an node is checked (only if tie_selection in checkbox settings is false) - * @event - * @name check_node.jstree - * @param {Object} node - * @param {Array} selected the current selection - * @param {Object} event the event (if any) that triggered this check_node - * @plugin checkbox - */ - this.trigger('check_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e }); - } - }; - /** - * uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally) - * @name uncheck_node(obj) - * @param {mixed} obj an array can be used to uncheck multiple nodes - * @trigger uncheck_node.jstree - * @plugin checkbox - */ - this.uncheck_node = function (obj, e) { - if(this.settings.checkbox.tie_selection) { return this.deselect_node(obj, false, e); } - var t1, t2, dom; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.uncheck_node(obj[t1], e); - } - return true; - } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { - return false; - } - dom = this.get_node(obj, true); - if(obj.state.checked) { - obj.state.checked = false; - this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, obj.id); - if(dom.length) { - dom.children('.jstree-anchor').removeClass('jstree-checked'); - } - /** - * triggered when an node is unchecked (only if tie_selection in checkbox settings is false) - * @event - * @name uncheck_node.jstree - * @param {Object} node - * @param {Array} selected the current selection - * @param {Object} event the event (if any) that triggered this uncheck_node - * @plugin checkbox - */ - this.trigger('uncheck_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e }); - } - }; - /** - * checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally) - * @name check_all() - * @trigger check_all.jstree, changed.jstree - * @plugin checkbox - */ - this.check_all = function () { - if(this.settings.checkbox.tie_selection) { return this.select_all(); } - var tmp = this._data.checkbox.selected.concat([]), i, j; - this._data.checkbox.selected = this._model.data[$.jstree.root].children_d.concat(); - for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) { - if(this._model.data[this._data.checkbox.selected[i]]) { - this._model.data[this._data.checkbox.selected[i]].state.checked = true; - } - } - this.redraw(true); - /** - * triggered when all nodes are checked (only if tie_selection in checkbox settings is false) - * @event - * @name check_all.jstree - * @param {Array} selected the current selection - * @plugin checkbox - */ - this.trigger('check_all', { 'selected' : this._data.checkbox.selected }); - }; - /** - * uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally) - * @name uncheck_all() - * @trigger uncheck_all.jstree - * @plugin checkbox - */ - this.uncheck_all = function () { - if(this.settings.checkbox.tie_selection) { return this.deselect_all(); } - var tmp = this._data.checkbox.selected.concat([]), i, j; - for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) { - if(this._model.data[this._data.checkbox.selected[i]]) { - this._model.data[this._data.checkbox.selected[i]].state.checked = false; - } - } - this._data.checkbox.selected = []; - this.element.find('.jstree-checked').removeClass('jstree-checked'); - /** - * triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false) - * @event - * @name uncheck_all.jstree - * @param {Object} node the previous selection - * @param {Array} selected the current selection - * @plugin checkbox - */ - this.trigger('uncheck_all', { 'selected' : this._data.checkbox.selected, 'node' : tmp }); - }; - /** - * checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected) - * @name is_checked(obj) - * @param {mixed} obj - * @return {Boolean} - * @plugin checkbox - */ - this.is_checked = function (obj) { - if(this.settings.checkbox.tie_selection) { return this.is_selected(obj); } - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - return obj.state.checked; - }; - /** - * get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected) - * @name get_checked([full]) - * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned - * @return {Array} - * @plugin checkbox - */ - this.get_checked = function (full) { - if(this.settings.checkbox.tie_selection) { return this.get_selected(full); } - return full ? $.map(this._data.checkbox.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.checkbox.selected; - }; - /** - * get an array of all top level checked nodes (ignoring children of checked nodes) (if tie_selection is on in the settings this function will return the same as get_top_selected) - * @name get_top_checked([full]) - * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned - * @return {Array} - * @plugin checkbox - */ - this.get_top_checked = function (full) { - if(this.settings.checkbox.tie_selection) { return this.get_top_selected(full); } - var tmp = this.get_checked(true), - obj = {}, i, j, k, l; - for(i = 0, j = tmp.length; i < j; i++) { - obj[tmp[i].id] = tmp[i]; - } - for(i = 0, j = tmp.length; i < j; i++) { - for(k = 0, l = tmp[i].children_d.length; k < l; k++) { - if(obj[tmp[i].children_d[k]]) { - delete obj[tmp[i].children_d[k]]; - } - } - } - tmp = []; - for(i in obj) { - if(obj.hasOwnProperty(i)) { - tmp.push(i); - } - } - return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp; - }; - /** - * get an array of all bottom level checked nodes (ignoring selected parents) (if tie_selection is on in the settings this function will return the same as get_bottom_selected) - * @name get_bottom_checked([full]) - * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned - * @return {Array} - * @plugin checkbox - */ - this.get_bottom_checked = function (full) { - if(this.settings.checkbox.tie_selection) { return this.get_bottom_selected(full); } - var tmp = this.get_checked(true), - obj = [], i, j; - for(i = 0, j = tmp.length; i < j; i++) { - if(!tmp[i].children.length) { - obj.push(tmp[i].id); - } - } - return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj; - }; - this.load_node = function (obj, callback) { - var k, l, i, j, c, tmp; - if(!$.isArray(obj) && !this.settings.checkbox.tie_selection) { - tmp = this.get_node(obj); - if(tmp && tmp.state.loaded) { - for(k = 0, l = tmp.children_d.length; k < l; k++) { - if(this._model.data[tmp.children_d[k]].state.checked) { - c = true; - this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, tmp.children_d[k]); - } - } - } - } - return parent.load_node.apply(this, arguments); - }; - this.get_state = function () { - var state = parent.get_state.apply(this, arguments); - if(this.settings.checkbox.tie_selection) { return state; } - state.checkbox = this._data.checkbox.selected.slice(); - return state; - }; - this.set_state = function (state, callback) { - var res = parent.set_state.apply(this, arguments); - if(res && state.checkbox) { - if(!this.settings.checkbox.tie_selection) { - this.uncheck_all(); - var _this = this; - $.each(state.checkbox, function (i, v) { - _this.check_node(v); - }); - } - delete state.checkbox; - this.set_state(state, callback); - return false; - } - return res; - }; - this.refresh = function (skip_loading, forget_state) { - if(!this.settings.checkbox.tie_selection) { - this._data.checkbox.selected = []; - } - return parent.refresh.apply(this, arguments); - }; - }; - - // include the checkbox plugin by default - // $.jstree.defaults.plugins.push("checkbox"); - -/** - * ### Conditionalselect plugin - * - * This plugin allows defining a callback to allow or deny node selection by user input (activate node method). - */ - - /** - * a callback (function) which is invoked in the instance's scope and receives two arguments - the node and the event that triggered the `activate_node` call. Returning false prevents working with the node, returning true allows invoking activate_node. Defaults to returning `true`. - * @name $.jstree.defaults.checkbox.visible - * @plugin checkbox - */ - $.jstree.defaults.conditionalselect = function () { return true; }; - $.jstree.plugins.conditionalselect = function (options, parent) { - // own function - this.activate_node = function (obj, e) { - if(this.settings.conditionalselect.call(this, this.get_node(obj), e)) { - parent.activate_node.call(this, obj, e); - } - }; - }; - - -/** - * ### Contextmenu plugin - * - * Shows a context menu when a node is right-clicked. - */ - - /** - * stores all defaults for the contextmenu plugin - * @name $.jstree.defaults.contextmenu - * @plugin contextmenu - */ - $.jstree.defaults.contextmenu = { - /** - * a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`. - * @name $.jstree.defaults.contextmenu.select_node - * @plugin contextmenu - */ - select_node : true, - /** - * a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used. - * @name $.jstree.defaults.contextmenu.show_at_node - * @plugin contextmenu - */ - show_at_node : true, - /** - * an object of actions, or a function that accepts a node and a callback function and calls the callback function with an object of actions available for that node (you can also return the items too). - * - * Each action consists of a key (a unique name) and a value which is an object with the following properties (only label and action are required): - * - * * `separator_before` - a boolean indicating if there should be a separator before this item - * * `separator_after` - a boolean indicating if there should be a separator after this item - * * `_disabled` - a boolean indicating if this action should be disabled - * * `label` - a string - the name of the action (could be a function returning a string) - * * `action` - a function to be executed if this item is chosen - * * `icon` - a string, can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class - * * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2) - * * `shortcut_label` - shortcut label (like for example `F2` for rename) - * - * @name $.jstree.defaults.contextmenu.items - * @plugin contextmenu - */ - items : function (o, cb) { // Could be an object directly - return { - "create" : { - "separator_before" : false, - "separator_after" : true, - "_disabled" : false, //(this.check("create_node", data.reference, {}, "last")), - "label" : "Create", - "action" : function (data) { - var inst = $.jstree.reference(data.reference), - obj = inst.get_node(data.reference); - inst.create_node(obj, {}, "last", function (new_node) { - setTimeout(function () { inst.edit(new_node); },0); - }); - } - }, - "rename" : { - "separator_before" : false, - "separator_after" : false, - "_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")), - "label" : "Rename", - /*! - "shortcut" : 113, - "shortcut_label" : 'F2', - "icon" : "glyphicon glyphicon-leaf", - */ - "action" : function (data) { - var inst = $.jstree.reference(data.reference), - obj = inst.get_node(data.reference); - inst.edit(obj); - } - }, - "remove" : { - "separator_before" : false, - "icon" : false, - "separator_after" : false, - "_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")), - "label" : "Delete", - "action" : function (data) { - var inst = $.jstree.reference(data.reference), - obj = inst.get_node(data.reference); - if(inst.is_selected(obj)) { - inst.delete_node(inst.get_selected()); - } - else { - inst.delete_node(obj); - } - } - }, - "ccp" : { - "separator_before" : true, - "icon" : false, - "separator_after" : false, - "label" : "Edit", - "action" : false, - "submenu" : { - "cut" : { - "separator_before" : false, - "separator_after" : false, - "label" : "Cut", - "action" : function (data) { - var inst = $.jstree.reference(data.reference), - obj = inst.get_node(data.reference); - if(inst.is_selected(obj)) { - inst.cut(inst.get_top_selected()); - } - else { - inst.cut(obj); - } - } - }, - "copy" : { - "separator_before" : false, - "icon" : false, - "separator_after" : false, - "label" : "Copy", - "action" : function (data) { - var inst = $.jstree.reference(data.reference), - obj = inst.get_node(data.reference); - if(inst.is_selected(obj)) { - inst.copy(inst.get_top_selected()); - } - else { - inst.copy(obj); - } - } - }, - "paste" : { - "separator_before" : false, - "icon" : false, - "_disabled" : function (data) { - return !$.jstree.reference(data.reference).can_paste(); - }, - "separator_after" : false, - "label" : "Paste", - "action" : function (data) { - var inst = $.jstree.reference(data.reference), - obj = inst.get_node(data.reference); - inst.paste(obj); - } - } - } - } - }; - } - }; - - $.jstree.plugins.contextmenu = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - - var last_ts = 0, cto = null, ex, ey; - this.element - .on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e, data) { - e.preventDefault(); - last_ts = e.ctrlKey ? +new Date() : 0; - if(data || cto) { - last_ts = (+new Date()) + 10000; - } - if(cto) { - clearTimeout(cto); - } - if(!this.is_loading(e.currentTarget)) { - this.show_contextmenu(e.currentTarget, e.pageX, e.pageY, e); - } - }, this)) - .on("click.jstree", ".jstree-anchor", $.proxy(function (e) { - if(this._data.contextmenu.visible && (!last_ts || (+new Date()) - last_ts > 250)) { // work around safari & macOS ctrl+click - $.vakata.context.hide(); - } - last_ts = 0; - }, this)) - .on("touchstart.jstree", ".jstree-anchor", function (e) { - if(!e.originalEvent || !e.originalEvent.changedTouches || !e.originalEvent.changedTouches[0]) { - return; - } - ex = e.pageX; - ey = e.pageY; - cto = setTimeout(function () { - $(e.currentTarget).trigger('contextmenu', true); - }, 750); - }) - .on('touchmove.vakata.jstree', function (e) { - if(cto && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0] && (Math.abs(ex - e.pageX) > 50 || Math.abs(ey - e.pageY) > 50)) { - clearTimeout(cto); - } - }) - .on('touchend.vakata.jstree', function (e) { - if(cto) { - clearTimeout(cto); - } - }); - - /*! - if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) { - var el = null, tm = null; - this.element - .on("touchstart", ".jstree-anchor", function (e) { - el = e.currentTarget; - tm = +new Date(); - $(document).one("touchend", function (e) { - e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset); - e.currentTarget = e.target; - tm = ((+(new Date())) - tm); - if(e.target === el && tm > 600 && tm < 1000) { - e.preventDefault(); - $(el).trigger('contextmenu', e); - } - el = null; - tm = null; - }); - }); - } - */ - $(document).on("context_hide.vakata.jstree", $.proxy(function () { this._data.contextmenu.visible = false; }, this)); - }; - this.teardown = function () { - if(this._data.contextmenu.visible) { - $.vakata.context.hide(); - } - parent.teardown.call(this); - }; - - /** - * prepare and show the context menu for a node - * @name show_contextmenu(obj [, x, y]) - * @param {mixed} obj the node - * @param {Number} x the x-coordinate relative to the document to show the menu at - * @param {Number} y the y-coordinate relative to the document to show the menu at - * @param {Object} e the event if available that triggered the contextmenu - * @plugin contextmenu - * @trigger show_contextmenu.jstree - */ - this.show_contextmenu = function (obj, x, y, e) { - obj = this.get_node(obj); - if(!obj || obj.id === $.jstree.root) { return false; } - var s = this.settings.contextmenu, - d = this.get_node(obj, true), - a = d.children(".jstree-anchor"), - o = false, - i = false; - if(s.show_at_node || x === undefined || y === undefined) { - o = a.offset(); - x = o.left; - y = o.top + this._data.core.li_height; - } - if(this.settings.contextmenu.select_node && !this.is_selected(obj)) { - this.activate_node(obj, e); - } - - i = s.items; - if($.isFunction(i)) { - i = i.call(this, obj, $.proxy(function (i) { - this._show_contextmenu(obj, x, y, i); - }, this)); - } - if($.isPlainObject(i)) { - this._show_contextmenu(obj, x, y, i); - } - }; - /** - * show the prepared context menu for a node - * @name _show_contextmenu(obj, x, y, i) - * @param {mixed} obj the node - * @param {Number} x the x-coordinate relative to the document to show the menu at - * @param {Number} y the y-coordinate relative to the document to show the menu at - * @param {Number} i the object of items to show - * @plugin contextmenu - * @trigger show_contextmenu.jstree - * @private - */ - this._show_contextmenu = function (obj, x, y, i) { - var d = this.get_node(obj, true), - a = d.children(".jstree-anchor"); - $(document).one("context_show.vakata.jstree", $.proxy(function (e, data) { - var cls = 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu'; - $(data.element).addClass(cls); - }, this)); - this._data.contextmenu.visible = true; - $.vakata.context.show(a, { 'x' : x, 'y' : y }, i); - /** - * triggered when the contextmenu is shown for a node - * @event - * @name show_contextmenu.jstree - * @param {Object} node the node - * @param {Number} x the x-coordinate of the menu relative to the document - * @param {Number} y the y-coordinate of the menu relative to the document - * @plugin contextmenu - */ - this.trigger('show_contextmenu', { "node" : obj, "x" : x, "y" : y }); - }; - }; - - // contextmenu helper - (function ($) { - var right_to_left = false, - vakata_context = { - element : false, - reference : false, - position_x : 0, - position_y : 0, - items : [], - html : "", - is_visible : false - }; - - $.vakata.context = { - settings : { - hide_onmouseleave : 0, - icons : true - }, - _trigger : function (event_name) { - $(document).triggerHandler("context_" + event_name + ".vakata", { - "reference" : vakata_context.reference, - "element" : vakata_context.element, - "position" : { - "x" : vakata_context.position_x, - "y" : vakata_context.position_y - } - }); - }, - _execute : function (i) { - i = vakata_context.items[i]; - return i && (!i._disabled || ($.isFunction(i._disabled) && !i._disabled({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }))) && i.action ? i.action.call(null, { - "item" : i, - "reference" : vakata_context.reference, - "element" : vakata_context.element, - "position" : { - "x" : vakata_context.position_x, - "y" : vakata_context.position_y - } - }) : false; - }, - _parse : function (o, is_callback) { - if(!o) { return false; } - if(!is_callback) { - vakata_context.html = ""; - vakata_context.items = []; - } - var str = "", - sep = false, - tmp; - - if(is_callback) { str += "<"+"ul>"; } - $.each(o, function (i, val) { - if(!val) { return true; } - vakata_context.items.push(val); - if(!sep && val.separator_before) { - str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>"; - } - sep = false; - str += "<"+"li class='" + (val._class || "") + (val._disabled === true || ($.isFunction(val._disabled) && val._disabled({ "item" : val, "reference" : vakata_context.reference, "element" : vakata_context.element })) ? " vakata-contextmenu-disabled " : "") + "' "+(val.shortcut?" data-shortcut='"+val.shortcut+"' ":'')+">"; - str += "<"+"a href='#' rel='" + (vakata_context.items.length - 1) + "'>"; - if($.vakata.context.settings.icons) { - str += "<"+"i "; - if(val.icon) { - if(val.icon.indexOf("/") !== -1 || val.icon.indexOf(".") !== -1) { str += " style='background:url(\"" + val.icon + "\") center center no-repeat' "; } - else { str += " class='" + val.icon + "' "; } - } - str += "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>"; - } - str += ($.isFunction(val.label) ? val.label({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }) : val.label) + (val.shortcut?' '+ (val.shortcut_label || '') +'':'') + "<"+"/a>"; - if(val.submenu) { - tmp = $.vakata.context._parse(val.submenu, true); - if(tmp) { str += tmp; } - } - str += "<"+"/li>"; - if(val.separator_after) { - str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>"; - sep = true; - } - }); - str = str.replace(/
  • <\/li\>$/,""); - if(is_callback) { str += ""; } - /** - * triggered on the document when the contextmenu is parsed (HTML is built) - * @event - * @plugin contextmenu - * @name context_parse.vakata - * @param {jQuery} reference the element that was right clicked - * @param {jQuery} element the DOM element of the menu itself - * @param {Object} position the x & y coordinates of the menu - */ - if(!is_callback) { vakata_context.html = str; $.vakata.context._trigger("parse"); } - return str.length > 10 ? str : false; - }, - _show_submenu : function (o) { - o = $(o); - if(!o.length || !o.children("ul").length) { return; } - var e = o.children("ul"), - x = o.offset().left + o.outerWidth(), - y = o.offset().top, - w = e.width(), - h = e.height(), - dw = $(window).width() + $(window).scrollLeft(), - dh = $(window).height() + $(window).scrollTop(); - // може да се спести е една проверка - дали няма някой от класовете вече нагоре - if(right_to_left) { - o[x - (w + 10 + o.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left"); - } - else { - o[x + w + 10 > dw ? "addClass" : "removeClass"]("vakata-context-right"); - } - if(y + h + 10 > dh) { - e.css("bottom","-1px"); - } - e.show(); - }, - show : function (reference, position, data) { - var o, e, x, y, w, h, dw, dh, cond = true; - if(vakata_context.element && vakata_context.element.length) { - vakata_context.element.width(''); - } - switch(cond) { - case (!position && !reference): - return false; - case (!!position && !!reference): - vakata_context.reference = reference; - vakata_context.position_x = position.x; - vakata_context.position_y = position.y; - break; - case (!position && !!reference): - vakata_context.reference = reference; - o = reference.offset(); - vakata_context.position_x = o.left + reference.outerHeight(); - vakata_context.position_y = o.top; - break; - case (!!position && !reference): - vakata_context.position_x = position.x; - vakata_context.position_y = position.y; - break; - } - if(!!reference && !data && $(reference).data('vakata_contextmenu')) { - data = $(reference).data('vakata_contextmenu'); - } - if($.vakata.context._parse(data)) { - vakata_context.element.html(vakata_context.html); - } - if(vakata_context.items.length) { - vakata_context.element.appendTo("body"); - e = vakata_context.element; - x = vakata_context.position_x; - y = vakata_context.position_y; - w = e.width(); - h = e.height(); - dw = $(window).width() + $(window).scrollLeft(); - dh = $(window).height() + $(window).scrollTop(); - if(right_to_left) { - x -= (e.outerWidth() - $(reference).outerWidth()); - if(x < $(window).scrollLeft() + 20) { - x = $(window).scrollLeft() + 20; - } - } - if(x + w + 20 > dw) { - x = dw - (w + 20); - } - if(y + h + 20 > dh) { - y = dh - (h + 20); - } - - vakata_context.element - .css({ "left" : x, "top" : y }) - .show() - .find('a').first().focus().parent().addClass("vakata-context-hover"); - vakata_context.is_visible = true; - /** - * triggered on the document when the contextmenu is shown - * @event - * @plugin contextmenu - * @name context_show.vakata - * @param {jQuery} reference the element that was right clicked - * @param {jQuery} element the DOM element of the menu itself - * @param {Object} position the x & y coordinates of the menu - */ - $.vakata.context._trigger("show"); - } - }, - hide : function () { - if(vakata_context.is_visible) { - vakata_context.element.hide().find("ul").hide().end().find(':focus').blur().end().detach(); - vakata_context.is_visible = false; - /** - * triggered on the document when the contextmenu is hidden - * @event - * @plugin contextmenu - * @name context_hide.vakata - * @param {jQuery} reference the element that was right clicked - * @param {jQuery} element the DOM element of the menu itself - * @param {Object} position the x & y coordinates of the menu - */ - $.vakata.context._trigger("hide"); - } - } - }; - $(function () { - right_to_left = $("body").css("direction") === "rtl"; - var to = false; - - vakata_context.element = $("
      "); - vakata_context.element - .on("mouseenter", "li", function (e) { - e.stopImmediatePropagation(); - - if($.contains(this, e.relatedTarget)) { - // премахнато заради delegate mouseleave по-долу - // $(this).find(".vakata-context-hover").removeClass("vakata-context-hover"); - return; - } - - if(to) { clearTimeout(to); } - vakata_context.element.find(".vakata-context-hover").removeClass("vakata-context-hover").end(); - - $(this) - .siblings().find("ul").hide().end().end() - .parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover"); - $.vakata.context._show_submenu(this); - }) - // тестово - дали не натоварва? - .on("mouseleave", "li", function (e) { - if($.contains(this, e.relatedTarget)) { return; } - $(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover"); - }) - .on("mouseleave", function (e) { - $(this).find(".vakata-context-hover").removeClass("vakata-context-hover"); - if($.vakata.context.settings.hide_onmouseleave) { - to = setTimeout( - (function (t) { - return function () { $.vakata.context.hide(); }; - }(this)), $.vakata.context.settings.hide_onmouseleave); - } - }) - .on("click", "a", function (e) { - e.preventDefault(); - //}) - //.on("mouseup", "a", function (e) { - if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata.context._execute($(this).attr("rel")) !== false) { - $.vakata.context.hide(); - } - }) - .on('keydown', 'a', function (e) { - var o = null; - switch(e.which) { - case 13: - case 32: - e.type = "mouseup"; - e.preventDefault(); - $(e.currentTarget).trigger(e); - break; - case 37: - if(vakata_context.is_visible) { - vakata_context.element.find(".vakata-context-hover").last().closest("li").first().find("ul").hide().find(".vakata-context-hover").removeClass("vakata-context-hover").end().end().children('a').focus(); - e.stopImmediatePropagation(); - e.preventDefault(); - } - break; - case 38: - if(vakata_context.is_visible) { - o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first(); - if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); } - o.addClass("vakata-context-hover").children('a').focus(); - e.stopImmediatePropagation(); - e.preventDefault(); - } - break; - case 39: - if(vakata_context.is_visible) { - vakata_context.element.find(".vakata-context-hover").last().children("ul").show().children("li:not(.vakata-context-separator)").removeClass("vakata-context-hover").first().addClass("vakata-context-hover").children('a').focus(); - e.stopImmediatePropagation(); - e.preventDefault(); - } - break; - case 40: - if(vakata_context.is_visible) { - o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first(); - if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); } - o.addClass("vakata-context-hover").children('a').focus(); - e.stopImmediatePropagation(); - e.preventDefault(); - } - break; - case 27: - $.vakata.context.hide(); - e.preventDefault(); - break; - default: - //console.log(e.which); - break; - } - }) - .on('keydown', function (e) { - e.preventDefault(); - var a = vakata_context.element.find('.vakata-contextmenu-shortcut-' + e.which).parent(); - if(a.parent().not('.vakata-context-disabled')) { - a.click(); - } - }); - - $(document) - .on("mousedown.vakata.jstree", function (e) { - if(vakata_context.is_visible && !$.contains(vakata_context.element[0], e.target)) { - $.vakata.context.hide(); - } - }) - .on("context_show.vakata.jstree", function (e, data) { - vakata_context.element.find("li:has(ul)").children("a").addClass("vakata-context-parent"); - if(right_to_left) { - vakata_context.element.addClass("vakata-context-rtl").css("direction", "rtl"); - } - // also apply a RTL class? - vakata_context.element.find("ul").hide().end(); - }); - }); - }($)); - // $.jstree.defaults.plugins.push("contextmenu"); - -/** - * ### Drag'n'drop plugin - * - * Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations. - */ - - /** - * stores all defaults for the drag'n'drop plugin - * @name $.jstree.defaults.dnd - * @plugin dnd - */ - $.jstree.defaults.dnd = { - /** - * a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`. - * @name $.jstree.defaults.dnd.copy - * @plugin dnd - */ - copy : true, - /** - * a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`. - * @name $.jstree.defaults.dnd.open_timeout - * @plugin dnd - */ - open_timeout : 500, - /** - * a function invoked each time a node is about to be dragged, invoked in the tree's scope and receives the nodes about to be dragged as an argument (array) and the event that started the drag - return `false` to prevent dragging - * @name $.jstree.defaults.dnd.is_draggable - * @plugin dnd - */ - is_draggable : true, - /** - * a boolean indicating if checks should constantly be made while the user is dragging the node (as opposed to checking only on drop), default is `true` - * @name $.jstree.defaults.dnd.check_while_dragging - * @plugin dnd - */ - check_while_dragging : true, - /** - * a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false` - * @name $.jstree.defaults.dnd.always_copy - * @plugin dnd - */ - always_copy : false, - /** - * when dropping a node "inside", this setting indicates the position the node should go to - it can be an integer or a string: "first" (same as 0) or "last", default is `0` - * @name $.jstree.defaults.dnd.inside_pos - * @plugin dnd - */ - inside_pos : 0, - /** - * when starting the drag on a node that is selected this setting controls if all selected nodes are dragged or only the single node, default is `true`, which means all selected nodes are dragged when the drag is started on a selected node - * @name $.jstree.defaults.dnd.drag_selection - * @plugin dnd - */ - drag_selection : true, - /** - * controls whether dnd works on touch devices. If left as boolean true dnd will work the same as in desktop browsers, which in some cases may impair scrolling. If set to boolean false dnd will not work on touch devices. There is a special third option - string "selected" which means only selected nodes can be dragged on touch devices. - * @name $.jstree.defaults.dnd.touch - * @plugin dnd - */ - touch : true, - /** - * controls whether items can be dropped anywhere on the node, not just on the anchor, by default only the node anchor is a valid drop target. Works best with the wholerow plugin. If enabled on mobile depending on the interface it might be hard for the user to cancel the drop, since the whole tree container will be a valid drop target. - * @name $.jstree.defaults.dnd.large_drop_target - * @plugin dnd - */ - large_drop_target : false, - /** - * controls whether a drag can be initiated from any part of the node and not just the text/icon part, works best with the wholerow plugin. Keep in mind it can cause problems with tree scrolling on mobile depending on the interface - in that case set the touch option to "selected". - * @name $.jstree.defaults.dnd.large_drag_target - * @plugin dnd - */ - large_drag_target : false - }; - // TODO: now check works by checking for each node individually, how about max_children, unique, etc? - $.jstree.plugins.dnd = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - - this.element - .on('mousedown.jstree touchstart.jstree', this.settings.dnd.large_drag_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) { - if(this.settings.dnd.large_drag_target && $(e.target).closest('.jstree-node')[0] !== e.currentTarget) { - return true; - } - if(e.type === "touchstart" && (!this.settings.dnd.touch || (this.settings.dnd.touch === 'selected' && !$(e.currentTarget).closest('.jstree-node').children('.jstree-anchor').hasClass('jstree-clicked')))) { - return true; - } - var obj = this.get_node(e.target), - mlt = this.is_selected(obj) && this.settings.dnd.drag_selection ? this.get_top_selected().length : 1, - txt = (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget)); - if(this.settings.core.force_text) { - txt = $.vakata.html.escape(txt); - } - if(obj && obj.id && obj.id !== $.jstree.root && (e.which === 1 || e.type === "touchstart") && - (this.settings.dnd.is_draggable === true || ($.isFunction(this.settings.dnd.is_draggable) && this.settings.dnd.is_draggable.call(this, (mlt > 1 ? this.get_top_selected(true) : [obj]), e))) - ) { - this.element.trigger('mousedown.jstree'); - return $.vakata.dnd.start(e, { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_top_selected() : [obj.id] }, '
      ' + txt + '
      '); - } - }, this)); - }; - }; - - $(function() { - // bind only once for all instances - var lastmv = false, - laster = false, - lastev = false, - opento = false, - marker = $('
       
      ').hide(); //.appendTo('body'); - - $(document) - .on('dnd_start.vakata.jstree', function (e, data) { - lastmv = false; - lastev = false; - if(!data || !data.data || !data.data.jstree) { return; } - marker.appendTo('body'); //.show(); - }) - .on('dnd_move.vakata.jstree', function (e, data) { - if(opento) { clearTimeout(opento); } - if(!data || !data.data || !data.data.jstree) { return; } - - // if we are hovering the marker image do nothing (can happen on "inside" drags) - if(data.event.target.id && data.event.target.id === 'jstree-marker') { - return; - } - lastev = data.event; - - var ins = $.jstree.reference(data.event.target), - ref = false, - off = false, - rel = false, - tmp, l, t, h, p, i, o, ok, t1, t2, op, ps, pr, ip, tm; - // if we are over an instance - if(ins && ins._data && ins._data.dnd) { - marker.attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' )); - data.helper - .children().attr('class', 'jstree-' + ins.get_theme() + ' jstree-' + ins.get_theme() + '-' + ins.get_theme_variant() + ' ' + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' )) - .find('.jstree-copy').first()[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'show' : 'hide' ](); - - - // if are hovering the container itself add a new root node - if( (data.event.target === ins.element[0] || data.event.target === ins.get_container_ul()[0]) && ins.get_container_ul().children().length === 0) { - ok = true; - for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) { - ok = ok && ins.check( (data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)) ) ? "copy_node" : "move_node"), (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), $.jstree.root, 'last', { 'dnd' : true, 'ref' : ins.get_node($.jstree.root), 'pos' : 'i', 'origin' : data.data.origin, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }); - if(!ok) { break; } - } - if(ok) { - lastmv = { 'ins' : ins, 'par' : $.jstree.root, 'pos' : 'last' }; - marker.hide(); - data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok'); - return; - } - } - else { - // if we are hovering a tree node - ref = ins.settings.dnd.large_drop_target ? $(data.event.target).closest('.jstree-node').children('.jstree-anchor') : $(data.event.target).closest('.jstree-anchor'); - if(ref && ref.length && ref.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) { - off = ref.offset(); - rel = data.event.pageY - off.top; - h = ref.outerHeight(); - if(rel < h / 3) { - o = ['b', 'i', 'a']; - } - else if(rel > h - h / 3) { - o = ['a', 'i', 'b']; - } - else { - o = rel > h / 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a']; - } - $.each(o, function (j, v) { - switch(v) { - case 'b': - l = off.left - 6; - t = off.top; - p = ins.get_parent(ref); - i = ref.parent().index(); - break; - case 'i': - ip = ins.settings.dnd.inside_pos; - tm = ins.get_node(ref.parent()); - l = off.left - 2; - t = off.top + h / 2 + 1; - p = tm.id; - i = ip === 'first' ? 0 : (ip === 'last' ? tm.children.length : Math.min(ip, tm.children.length)); - break; - case 'a': - l = off.left - 6; - t = off.top + h; - p = ins.get_parent(ref); - i = ref.parent().index() + 1; - break; - } - ok = true; - for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) { - op = data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? "copy_node" : "move_node"; - ps = i; - if(op === "move_node" && v === 'a' && (data.data.origin && data.data.origin === ins) && p === ins.get_parent(data.data.nodes[t1])) { - pr = ins.get_node(p); - if(ps > $.inArray(data.data.nodes[t1], pr.children)) { - ps -= 1; - } - } - ok = ok && ( (ins && ins.settings && ins.settings.dnd && ins.settings.dnd.check_while_dragging === false) || ins.check(op, (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), p, ps, { 'dnd' : true, 'ref' : ins.get_node(ref.parent()), 'pos' : v, 'origin' : data.data.origin, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }) ); - if(!ok) { - if(ins && ins.last_error) { laster = ins.last_error(); } - break; - } - } - if(v === 'i' && ref.parent().is('.jstree-closed') && ins.settings.dnd.open_timeout) { - opento = setTimeout((function (x, z) { return function () { x.open_node(z); }; }(ins, ref)), ins.settings.dnd.open_timeout); - } - if(ok) { - lastmv = { 'ins' : ins, 'par' : p, 'pos' : v === 'i' && ip === 'last' && i === 0 && !ins.is_loaded(tm) ? 'last' : i }; - marker.css({ 'left' : l + 'px', 'top' : t + 'px' }).show(); - data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok'); - laster = {}; - o = true; - return false; - } - }); - if(o === true) { return; } - } - } - } - lastmv = false; - data.helper.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er'); - marker.hide(); - }) - .on('dnd_scroll.vakata.jstree', function (e, data) { - if(!data || !data.data || !data.data.jstree) { return; } - marker.hide(); - lastmv = false; - lastev = false; - data.helper.find('.jstree-icon').first().removeClass('jstree-ok').addClass('jstree-er'); - }) - .on('dnd_stop.vakata.jstree', function (e, data) { - if(opento) { clearTimeout(opento); } - if(!data || !data.data || !data.data.jstree) { return; } - marker.hide().detach(); - var i, j, nodes = []; - if(lastmv) { - for(i = 0, j = data.data.nodes.length; i < j; i++) { - nodes[i] = data.data.origin ? data.data.origin.get_node(data.data.nodes[i]) : data.data.nodes[i]; - } - lastmv.ins[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'copy_node' : 'move_node' ](nodes, lastmv.par, lastmv.pos, false, false, false, data.data.origin); - } - else { - i = $(data.event.target).closest('.jstree'); - if(i.length && laster && laster.error && laster.error === 'check') { - i = i.jstree(true); - if(i) { - i.settings.core.error.call(this, laster); - } - } - } - lastev = false; - lastmv = false; - }) - .on('keyup.jstree keydown.jstree', function (e, data) { - data = $.vakata.dnd._get(); - if(data && data.data && data.data.jstree) { - data.helper.find('.jstree-copy').first()[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (e.metaKey || e.ctrlKey))) ? 'show' : 'hide' ](); - if(lastev) { - lastev.metaKey = e.metaKey; - lastev.ctrlKey = e.ctrlKey; - $.vakata.dnd._trigger('move', lastev); - } - } - }); - }); - - // helpers - (function ($) { - $.vakata.html = { - div : $('
      '), - escape : function (str) { - return $.vakata.html.div.text(str).html(); - }, - strip : function (str) { - return $.vakata.html.div.empty().append($.parseHTML(str)).text(); - } - }; - // private variable - var vakata_dnd = { - element : false, - target : false, - is_down : false, - is_drag : false, - helper : false, - helper_w: 0, - data : false, - init_x : 0, - init_y : 0, - scroll_l: 0, - scroll_t: 0, - scroll_e: false, - scroll_i: false, - is_touch: false - }; - $.vakata.dnd = { - settings : { - scroll_speed : 10, - scroll_proximity : 20, - helper_left : 5, - helper_top : 10, - threshold : 5, - threshold_touch : 50 - }, - _trigger : function (event_name, e) { - var data = $.vakata.dnd._get(); - data.event = e; - $(document).triggerHandler("dnd_" + event_name + ".vakata", data); - }, - _get : function () { - return { - "data" : vakata_dnd.data, - "element" : vakata_dnd.element, - "helper" : vakata_dnd.helper - }; - }, - _clean : function () { - if(vakata_dnd.helper) { vakata_dnd.helper.remove(); } - if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; } - vakata_dnd = { - element : false, - target : false, - is_down : false, - is_drag : false, - helper : false, - helper_w: 0, - data : false, - init_x : 0, - init_y : 0, - scroll_l: 0, - scroll_t: 0, - scroll_e: false, - scroll_i: false, - is_touch: false - }; - $(document).off("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag); - $(document).off("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop); - }, - _scroll : function (init_only) { - if(!vakata_dnd.scroll_e || (!vakata_dnd.scroll_l && !vakata_dnd.scroll_t)) { - if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; } - return false; - } - if(!vakata_dnd.scroll_i) { - vakata_dnd.scroll_i = setInterval($.vakata.dnd._scroll, 100); - return false; - } - if(init_only === true) { return false; } - - var i = vakata_dnd.scroll_e.scrollTop(), - j = vakata_dnd.scroll_e.scrollLeft(); - vakata_dnd.scroll_e.scrollTop(i + vakata_dnd.scroll_t * $.vakata.dnd.settings.scroll_speed); - vakata_dnd.scroll_e.scrollLeft(j + vakata_dnd.scroll_l * $.vakata.dnd.settings.scroll_speed); - if(i !== vakata_dnd.scroll_e.scrollTop() || j !== vakata_dnd.scroll_e.scrollLeft()) { - /** - * triggered on the document when a drag causes an element to scroll - * @event - * @plugin dnd - * @name dnd_scroll.vakata - * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start - * @param {DOM} element the DOM element being dragged - * @param {jQuery} helper the helper shown next to the mouse - * @param {jQuery} event the element that is scrolling - */ - $.vakata.dnd._trigger("scroll", vakata_dnd.scroll_e); - } - }, - start : function (e, data, html) { - if(e.type === "touchstart" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) { - e.pageX = e.originalEvent.changedTouches[0].pageX; - e.pageY = e.originalEvent.changedTouches[0].pageY; - e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset); - } - if(vakata_dnd.is_drag) { $.vakata.dnd.stop({}); } - try { - e.currentTarget.unselectable = "on"; - e.currentTarget.onselectstart = function() { return false; }; - if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; } - } catch(ignore) { } - vakata_dnd.init_x = e.pageX; - vakata_dnd.init_y = e.pageY; - vakata_dnd.data = data; - vakata_dnd.is_down = true; - vakata_dnd.element = e.currentTarget; - vakata_dnd.target = e.target; - vakata_dnd.is_touch = e.type === "touchstart"; - if(html !== false) { - vakata_dnd.helper = $("
      ").html(html).css({ - "display" : "block", - "margin" : "0", - "padding" : "0", - "position" : "absolute", - "top" : "-2000px", - "lineHeight" : "16px", - "zIndex" : "10000" - }); - } - $(document).on("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag); - $(document).on("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop); - return false; - }, - drag : function (e) { - if(e.type === "touchmove" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) { - e.pageX = e.originalEvent.changedTouches[0].pageX; - e.pageY = e.originalEvent.changedTouches[0].pageY; - e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset); - } - if(!vakata_dnd.is_down) { return; } - if(!vakata_dnd.is_drag) { - if( - Math.abs(e.pageX - vakata_dnd.init_x) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) || - Math.abs(e.pageY - vakata_dnd.init_y) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) - ) { - if(vakata_dnd.helper) { - vakata_dnd.helper.appendTo("body"); - vakata_dnd.helper_w = vakata_dnd.helper.outerWidth(); - } - vakata_dnd.is_drag = true; - /** - * triggered on the document when a drag starts - * @event - * @plugin dnd - * @name dnd_start.vakata - * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start - * @param {DOM} element the DOM element being dragged - * @param {jQuery} helper the helper shown next to the mouse - * @param {Object} event the event that caused the start (probably mousemove) - */ - $.vakata.dnd._trigger("start", e); - } - else { return; } - } - - var d = false, w = false, - dh = false, wh = false, - dw = false, ww = false, - dt = false, dl = false, - ht = false, hl = false; - - vakata_dnd.scroll_t = 0; - vakata_dnd.scroll_l = 0; - vakata_dnd.scroll_e = false; - $($(e.target).parentsUntil("body").addBack().get().reverse()) - .filter(function () { - return (/^auto|scroll$/).test($(this).css("overflow")) && - (this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth); - }) - .each(function () { - var t = $(this), o = t.offset(); - if(this.scrollHeight > this.offsetHeight) { - if(o.top + t.height() - e.pageY < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; } - if(e.pageY - o.top < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; } - } - if(this.scrollWidth > this.offsetWidth) { - if(o.left + t.width() - e.pageX < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; } - if(e.pageX - o.left < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; } - } - if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) { - vakata_dnd.scroll_e = $(this); - return false; - } - }); - - if(!vakata_dnd.scroll_e) { - d = $(document); w = $(window); - dh = d.height(); wh = w.height(); - dw = d.width(); ww = w.width(); - dt = d.scrollTop(); dl = d.scrollLeft(); - if(dh > wh && e.pageY - dt < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; } - if(dh > wh && wh - (e.pageY - dt) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; } - if(dw > ww && e.pageX - dl < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; } - if(dw > ww && ww - (e.pageX - dl) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; } - if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) { - vakata_dnd.scroll_e = d; - } - } - if(vakata_dnd.scroll_e) { $.vakata.dnd._scroll(true); } - - if(vakata_dnd.helper) { - ht = parseInt(e.pageY + $.vakata.dnd.settings.helper_top, 10); - hl = parseInt(e.pageX + $.vakata.dnd.settings.helper_left, 10); - if(dh && ht + 25 > dh) { ht = dh - 50; } - if(dw && hl + vakata_dnd.helper_w > dw) { hl = dw - (vakata_dnd.helper_w + 2); } - vakata_dnd.helper.css({ - left : hl + "px", - top : ht + "px" - }); - } - /** - * triggered on the document when a drag is in progress - * @event - * @plugin dnd - * @name dnd_move.vakata - * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start - * @param {DOM} element the DOM element being dragged - * @param {jQuery} helper the helper shown next to the mouse - * @param {Object} event the event that caused this to trigger (most likely mousemove) - */ - $.vakata.dnd._trigger("move", e); - return false; - }, - stop : function (e) { - if(e.type === "touchend" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) { - e.pageX = e.originalEvent.changedTouches[0].pageX; - e.pageY = e.originalEvent.changedTouches[0].pageY; - e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset); - } - if(vakata_dnd.is_drag) { - /** - * triggered on the document when a drag stops (the dragged element is dropped) - * @event - * @plugin dnd - * @name dnd_stop.vakata - * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start - * @param {DOM} element the DOM element being dragged - * @param {jQuery} helper the helper shown next to the mouse - * @param {Object} event the event that caused the stop - */ - $.vakata.dnd._trigger("stop", e); - } - else { - if(e.type === "touchend" && e.target === vakata_dnd.target) { - var to = setTimeout(function () { $(e.target).click(); }, 100); - $(e.target).one('click', function() { if(to) { clearTimeout(to); } }); - } - } - $.vakata.dnd._clean(); - return false; - } - }; - }($)); - - // include the dnd plugin by default - // $.jstree.defaults.plugins.push("dnd"); - - -/** - * ### Massload plugin - * - * Adds massload functionality to jsTree, so that multiple nodes can be loaded in a single request (only useful with lazy loading). - */ - - /** - * massload configuration - * - * It is possible to set this to a standard jQuery-like AJAX config. - * In addition to the standard jQuery ajax options here you can supply functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node IDs need to be loaded, the return value of those functions will be used. - * - * You can also set this to a function, that function will receive the node IDs being loaded as argument and a second param which is a function (callback) which should be called with the result. - * - * Both the AJAX and the function approach rely on the same return value - an object where the keys are the node IDs, and the value is the children of that node as an array. - * - * { - * "id1" : [{ "text" : "Child of ID1", "id" : "c1" }, { "text" : "Another child of ID1", "id" : "c2" }], - * "id2" : [{ "text" : "Child of ID2", "id" : "c3" }] - * } - * - * @name $.jstree.defaults.massload - * @plugin massload - */ - $.jstree.defaults.massload = null; - $.jstree.plugins.massload = function (options, parent) { - this.init = function (el, options) { - parent.init.call(this, el, options); - this._data.massload = {}; - }; - this._load_nodes = function (nodes, callback, is_callback) { - var s = this.settings.massload; - if(is_callback && !$.isEmptyObject(this._data.massload)) { - return parent._load_nodes.call(this, nodes, callback, is_callback); - } - if($.isFunction(s)) { - return s.call(this, nodes, $.proxy(function (data) { - if(data) { - for(var i in data) { - if(data.hasOwnProperty(i)) { - this._data.massload[i] = data[i]; - } - } - } - parent._load_nodes.call(this, nodes, callback, is_callback); - }, this)); - } - if(typeof s === 'object' && s && s.url) { - s = $.extend(true, {}, s); - if($.isFunction(s.url)) { - s.url = s.url.call(this, nodes); - } - if($.isFunction(s.data)) { - s.data = s.data.call(this, nodes); - } - return $.ajax(s) - .done($.proxy(function (data,t,x) { - if(data) { - for(var i in data) { - if(data.hasOwnProperty(i)) { - this._data.massload[i] = data[i]; - } - } - } - parent._load_nodes.call(this, nodes, callback, is_callback); - }, this)) - .fail($.proxy(function (f) { - parent._load_nodes.call(this, nodes, callback, is_callback); - }, this)); - } - return parent._load_nodes.call(this, nodes, callback, is_callback); - }; - this._load_node = function (obj, callback) { - var d = this._data.massload[obj.id]; - if(d) { - return this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }) : d, function (status) { - callback.call(this, status); - delete this._data.massload[obj.id]; - }); - } - return parent._load_node.call(this, obj, callback); - }; - }; - -/** - * ### Search plugin - * - * Adds search functionality to jsTree. - */ - - /** - * stores all defaults for the search plugin - * @name $.jstree.defaults.search - * @plugin search - */ - $.jstree.defaults.search = { - /** - * a jQuery-like AJAX config, which jstree uses if a server should be queried for results. - * - * A `str` (which is the search string) parameter will be added with the request, an optional `inside` parameter will be added if the search is limited to a node id. The expected result is a JSON array with nodes that need to be opened so that matching nodes will be revealed. - * Leave this setting as `false` to not query the server. You can also set this to a function, which will be invoked in the instance's scope and receive 3 parameters - the search string, the callback to call with the array of nodes to load, and the optional node ID to limit the search to - * @name $.jstree.defaults.search.ajax - * @plugin search - */ - ajax : false, - /** - * Indicates if the search should be fuzzy or not (should `chnd3` match `child node 3`). Default is `false`. - * @name $.jstree.defaults.search.fuzzy - * @plugin search - */ - fuzzy : false, - /** - * Indicates if the search should be case sensitive. Default is `false`. - * @name $.jstree.defaults.search.case_sensitive - * @plugin search - */ - case_sensitive : false, - /** - * Indicates if the tree should be filtered (by default) to show only matching nodes (keep in mind this can be a heavy on large trees in old browsers). - * This setting can be changed at runtime when calling the search method. Default is `false`. - * @name $.jstree.defaults.search.show_only_matches - * @plugin search - */ - show_only_matches : false, - /** - * Indicates if the children of matched element are shown (when show_only_matches is true) - * This setting can be changed at runtime when calling the search method. Default is `false`. - * @name $.jstree.defaults.search.show_only_matches_children - * @plugin search - */ - show_only_matches_children : false, - /** - * Indicates if all nodes opened to reveal the search result, should be closed when the search is cleared or a new search is performed. Default is `true`. - * @name $.jstree.defaults.search.close_opened_onclear - * @plugin search - */ - close_opened_onclear : true, - /** - * Indicates if only leaf nodes should be included in search results. Default is `false`. - * @name $.jstree.defaults.search.search_leaves_only - * @plugin search - */ - search_leaves_only : false, - /** - * If set to a function it wil be called in the instance's scope with two arguments - search string and node (where node will be every node in the structure, so use with caution). - * If the function returns a truthy value the node will be considered a match (it might not be displayed if search_only_leaves is set to true and the node is not a leaf). Default is `false`. - * @name $.jstree.defaults.search.search_callback - * @plugin search - */ - search_callback : false - }; - - $.jstree.plugins.search = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - - this._data.search.str = ""; - this._data.search.dom = $(); - this._data.search.res = []; - this._data.search.opn = []; - this._data.search.som = false; - this._data.search.smc = false; - this._data.search.hdn = []; - - this.element - .on("search.jstree", $.proxy(function (e, data) { - if(this._data.search.som && data.res.length) { - var m = this._model.data, i, j, p = []; - for(i = 0, j = data.res.length; i < j; i++) { - if(m[data.res[i]] && !m[data.res[i]].state.hidden) { - p.push(data.res[i]); - p = p.concat(m[data.res[i]].parents); - if(this._data.search.smc) { - p = p.concat(m[data.res[i]].children_d); - } - } - } - p = $.vakata.array_remove_item($.vakata.array_unique(p), $.jstree.root); - this._data.search.hdn = this.hide_all(true); - this.show_node(p); - } - }, this)) - .on("clear_search.jstree", $.proxy(function (e, data) { - if(this._data.search.som && data.res.length) { - this.show_node(this._data.search.hdn); - } - }, this)); - }; - /** - * used to search the tree nodes for a given string - * @name search(str [, skip_async]) - * @param {String} str the search string - * @param {Boolean} skip_async if set to true server will not be queried even if configured - * @param {Boolean} show_only_matches if set to true only matching nodes will be shown (keep in mind this can be very slow on large trees or old browsers) - * @param {mixed} inside an optional node to whose children to limit the search - * @param {Boolean} append if set to true the results of this search are appended to the previous search - * @plugin search - * @trigger search.jstree - */ - this.search = function (str, skip_async, show_only_matches, inside, append, show_only_matches_children) { - if(str === false || $.trim(str.toString()) === "") { - return this.clear_search(); - } - inside = this.get_node(inside); - inside = inside && inside.id ? inside.id : null; - str = str.toString(); - var s = this.settings.search, - a = s.ajax ? s.ajax : false, - m = this._model.data, - f = null, - r = [], - p = [], i, j; - if(this._data.search.res.length && !append) { - this.clear_search(); - } - if(show_only_matches === undefined) { - show_only_matches = s.show_only_matches; - } - if(show_only_matches_children === undefined) { - show_only_matches_children = s.show_only_matches_children; - } - if(!skip_async && a !== false) { - if($.isFunction(a)) { - return a.call(this, str, $.proxy(function (d) { - if(d && d.d) { d = d.d; } - this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () { - this.search(str, true, show_only_matches, inside, append); - }, true); - }, this), inside); - } - else { - a = $.extend({}, a); - if(!a.data) { a.data = {}; } - a.data.str = str; - if(inside) { - a.data.inside = inside; - } - return $.ajax(a) - .fail($.proxy(function () { - this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'search', 'id' : 'search_01', 'reason' : 'Could not load search parents', 'data' : JSON.stringify(a) }; - this.settings.core.error.call(this, this._data.core.last_error); - }, this)) - .done($.proxy(function (d) { - if(d && d.d) { d = d.d; } - this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () { - this.search(str, true, show_only_matches, inside, append); - }, true); - }, this)); - } - } - if(!append) { - this._data.search.str = str; - this._data.search.dom = $(); - this._data.search.res = []; - this._data.search.opn = []; - this._data.search.som = show_only_matches; - this._data.search.smc = show_only_matches_children; - } - - f = new $.vakata.search(str, true, { caseSensitive : s.case_sensitive, fuzzy : s.fuzzy }); - $.each(m[inside ? inside : $.jstree.root].children_d, function (ii, i) { - var v = m[i]; - if(v.text && (!s.search_leaves_only || (v.state.loaded && v.children.length === 0)) && ( (s.search_callback && s.search_callback.call(this, str, v)) || (!s.search_callback && f.search(v.text).isMatch) ) ) { - r.push(i); - p = p.concat(v.parents); - } - }); - if(r.length) { - p = $.vakata.array_unique(p); - for(i = 0, j = p.length; i < j; i++) { - if(p[i] !== $.jstree.root && m[p[i]] && this.open_node(p[i], null, 0) === true) { - this._data.search.opn.push(p[i]); - } - } - if(!append) { - this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #'))); - this._data.search.res = r; - } - else { - this._data.search.dom = this._data.search.dom.add($(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #')))); - this._data.search.res = $.vakata.array_unique(this._data.search.res.concat(r)); - } - this._data.search.dom.children(".jstree-anchor").addClass('jstree-search'); - } - /** - * triggered after search is complete - * @event - * @name search.jstree - * @param {jQuery} nodes a jQuery collection of matching nodes - * @param {String} str the search string - * @param {Array} res a collection of objects represeing the matching nodes - * @plugin search - */ - this.trigger('search', { nodes : this._data.search.dom, str : str, res : this._data.search.res, show_only_matches : show_only_matches }); - }; - /** - * used to clear the last search (removes classes and shows all nodes if filtering is on) - * @name clear_search() - * @plugin search - * @trigger clear_search.jstree - */ - this.clear_search = function () { - if(this.settings.search.close_opened_onclear) { - this.close_node(this._data.search.opn, 0); - } - /** - * triggered after search is complete - * @event - * @name clear_search.jstree - * @param {jQuery} nodes a jQuery collection of matching nodes (the result from the last search) - * @param {String} str the search string (the last search string) - * @param {Array} res a collection of objects represeing the matching nodes (the result from the last search) - * @plugin search - */ - this.trigger('clear_search', { 'nodes' : this._data.search.dom, str : this._data.search.str, res : this._data.search.res }); - if(this._data.search.res.length) { - this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(this._data.search.res, function (v) { - return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); - }).join(', #'))); - this._data.search.dom.children(".jstree-anchor").removeClass("jstree-search"); - } - this._data.search.str = ""; - this._data.search.res = []; - this._data.search.opn = []; - this._data.search.dom = $(); - }; - - this.redraw_node = function(obj, deep, callback, force_render) { - obj = parent.redraw_node.apply(this, arguments); - if(obj) { - if($.inArray(obj.id, this._data.search.res) !== -1) { - var i, j, tmp = null; - for(i = 0, j = obj.childNodes.length; i < j; i++) { - if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) { - tmp = obj.childNodes[i]; - break; - } - } - if(tmp) { - tmp.className += ' jstree-search'; - } - } - } - return obj; - }; - }; - - // helpers - (function ($) { - // from http://kiro.me/projects/fuse.html - $.vakata.search = function(pattern, txt, options) { - options = options || {}; - options = $.extend({}, $.vakata.search.defaults, options); - if(options.fuzzy !== false) { - options.fuzzy = true; - } - pattern = options.caseSensitive ? pattern : pattern.toLowerCase(); - var MATCH_LOCATION = options.location, - MATCH_DISTANCE = options.distance, - MATCH_THRESHOLD = options.threshold, - patternLen = pattern.length, - matchmask, pattern_alphabet, match_bitapScore, search; - if(patternLen > 32) { - options.fuzzy = false; - } - if(options.fuzzy) { - matchmask = 1 << (patternLen - 1); - pattern_alphabet = (function () { - var mask = {}, - i = 0; - for (i = 0; i < patternLen; i++) { - mask[pattern.charAt(i)] = 0; - } - for (i = 0; i < patternLen; i++) { - mask[pattern.charAt(i)] |= 1 << (patternLen - i - 1); - } - return mask; - }()); - match_bitapScore = function (e, x) { - var accuracy = e / patternLen, - proximity = Math.abs(MATCH_LOCATION - x); - if(!MATCH_DISTANCE) { - return proximity ? 1.0 : accuracy; - } - return accuracy + (proximity / MATCH_DISTANCE); - }; - } - search = function (text) { - text = options.caseSensitive ? text : text.toLowerCase(); - if(pattern === text || text.indexOf(pattern) !== -1) { - return { - isMatch: true, - score: 0 - }; - } - if(!options.fuzzy) { - return { - isMatch: false, - score: 1 - }; - } - var i, j, - textLen = text.length, - scoreThreshold = MATCH_THRESHOLD, - bestLoc = text.indexOf(pattern, MATCH_LOCATION), - binMin, binMid, - binMax = patternLen + textLen, - lastRd, start, finish, rd, charMatch, - score = 1, - locations = []; - if (bestLoc !== -1) { - scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold); - bestLoc = text.lastIndexOf(pattern, MATCH_LOCATION + patternLen); - if (bestLoc !== -1) { - scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold); - } - } - bestLoc = -1; - for (i = 0; i < patternLen; i++) { - binMin = 0; - binMid = binMax; - while (binMin < binMid) { - if (match_bitapScore(i, MATCH_LOCATION + binMid) <= scoreThreshold) { - binMin = binMid; - } else { - binMax = binMid; - } - binMid = Math.floor((binMax - binMin) / 2 + binMin); - } - binMax = binMid; - start = Math.max(1, MATCH_LOCATION - binMid + 1); - finish = Math.min(MATCH_LOCATION + binMid, textLen) + patternLen; - rd = new Array(finish + 2); - rd[finish + 1] = (1 << i) - 1; - for (j = finish; j >= start; j--) { - charMatch = pattern_alphabet[text.charAt(j - 1)]; - if (i === 0) { - rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; - } else { - rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | (((lastRd[j + 1] | lastRd[j]) << 1) | 1) | lastRd[j + 1]; - } - if (rd[j] & matchmask) { - score = match_bitapScore(i, j - 1); - if (score <= scoreThreshold) { - scoreThreshold = score; - bestLoc = j - 1; - locations.push(bestLoc); - if (bestLoc > MATCH_LOCATION) { - start = Math.max(1, 2 * MATCH_LOCATION - bestLoc); - } else { - break; - } - } - } - } - if (match_bitapScore(i + 1, MATCH_LOCATION) > scoreThreshold) { - break; - } - lastRd = rd; - } - return { - isMatch: bestLoc >= 0, - score: score - }; - }; - return txt === true ? { 'search' : search } : search(txt); - }; - $.vakata.search.defaults = { - location : 0, - distance : 100, - threshold : 0.6, - fuzzy : false, - caseSensitive : false - }; - }($)); - - // include the search plugin by default - // $.jstree.defaults.plugins.push("search"); - - -/** - * ### Sort plugin - * - * Automatically sorts all siblings in the tree according to a sorting function. - */ - - /** - * the settings function used to sort the nodes. - * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`. - * @name $.jstree.defaults.sort - * @plugin sort - */ - $.jstree.defaults.sort = function (a, b) { - //return this.get_type(a) === this.get_type(b) ? (this.get_text(a) > this.get_text(b) ? 1 : -1) : this.get_type(a) >= this.get_type(b); - return this.get_text(a) > this.get_text(b) ? 1 : -1; - }; - $.jstree.plugins.sort = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - this.element - .on("model.jstree", $.proxy(function (e, data) { - this.sort(data.parent, true); - }, this)) - .on("rename_node.jstree create_node.jstree", $.proxy(function (e, data) { - this.sort(data.parent || data.node.parent, false); - this.redraw_node(data.parent || data.node.parent, true); - }, this)) - .on("move_node.jstree copy_node.jstree", $.proxy(function (e, data) { - this.sort(data.parent, false); - this.redraw_node(data.parent, true); - }, this)); - }; - /** - * used to sort a node's children - * @private - * @name sort(obj [, deep]) - * @param {mixed} obj the node - * @param {Boolean} deep if set to `true` nodes are sorted recursively. - * @plugin sort - * @trigger search.jstree - */ - this.sort = function (obj, deep) { - var i, j; - obj = this.get_node(obj); - if(obj && obj.children && obj.children.length) { - obj.children.sort($.proxy(this.settings.sort, this)); - if(deep) { - for(i = 0, j = obj.children_d.length; i < j; i++) { - this.sort(obj.children_d[i], false); - } - } - } - }; - }; - - // include the sort plugin by default - // $.jstree.defaults.plugins.push("sort"); - -/** - * ### State plugin - * - * Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc) - */ - - var to = false; - /** - * stores all defaults for the state plugin - * @name $.jstree.defaults.state - * @plugin state - */ - $.jstree.defaults.state = { - /** - * A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`. - * @name $.jstree.defaults.state.key - * @plugin state - */ - key : 'jstree', - /** - * A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`. - * @name $.jstree.defaults.state.events - * @plugin state - */ - events : 'changed.jstree open_node.jstree close_node.jstree check_node.jstree uncheck_node.jstree', - /** - * Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire. - * @name $.jstree.defaults.state.ttl - * @plugin state - */ - ttl : false, - /** - * A function that will be executed prior to restoring state with one argument - the state object. Can be used to clear unwanted parts of the state. - * @name $.jstree.defaults.state.filter - * @plugin state - */ - filter : false - }; - $.jstree.plugins.state = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - var bind = $.proxy(function () { - this.element.on(this.settings.state.events, $.proxy(function () { - if(to) { clearTimeout(to); } - to = setTimeout($.proxy(function () { this.save_state(); }, this), 100); - }, this)); - /** - * triggered when the state plugin is finished restoring the state (and immediately after ready if there is no state to restore). - * @event - * @name state_ready.jstree - * @plugin state - */ - this.trigger('state_ready'); - }, this); - this.element - .on("ready.jstree", $.proxy(function (e, data) { - this.element.one("restore_state.jstree", bind); - if(!this.restore_state()) { bind(); } - }, this)); - }; - /** - * save the state - * @name save_state() - * @plugin state - */ - this.save_state = function () { - var st = { 'state' : this.get_state(), 'ttl' : this.settings.state.ttl, 'sec' : +(new Date()) }; - $.vakata.storage.set(this.settings.state.key, JSON.stringify(st)); - }; - /** - * restore the state from the user's computer - * @name restore_state() - * @plugin state - */ - this.restore_state = function () { - var k = $.vakata.storage.get(this.settings.state.key); - if(!!k) { try { k = JSON.parse(k); } catch(ex) { return false; } } - if(!!k && k.ttl && k.sec && +(new Date()) - k.sec > k.ttl) { return false; } - if(!!k && k.state) { k = k.state; } - if(!!k && $.isFunction(this.settings.state.filter)) { k = this.settings.state.filter.call(this, k); } - if(!!k) { - this.element.one("set_state.jstree", function (e, data) { data.instance.trigger('restore_state', { 'state' : $.extend(true, {}, k) }); }); - this.set_state(k); - return true; - } - return false; - }; - /** - * clear the state on the user's computer - * @name clear_state() - * @plugin state - */ - this.clear_state = function () { - return $.vakata.storage.del(this.settings.state.key); - }; - }; - - (function ($, undefined) { - $.vakata.storage = { - // simply specifying the functions in FF throws an error - set : function (key, val) { return window.localStorage.setItem(key, val); }, - get : function (key) { return window.localStorage.getItem(key); }, - del : function (key) { return window.localStorage.removeItem(key); } - }; - }($)); - - // include the state plugin by default - // $.jstree.defaults.plugins.push("state"); - -/** - * ### Types plugin - * - * Makes it possible to add predefined types for groups of nodes, which make it possible to easily control nesting rules and icon for each group. - */ - - /** - * An object storing all types as key value pairs, where the key is the type name and the value is an object that could contain following keys (all optional). - * - * * `max_children` the maximum number of immediate children this node type can have. Do not specify or set to `-1` for unlimited. - * * `max_depth` the maximum number of nesting this node type can have. A value of `1` would mean that the node can have children, but no grandchildren. Do not specify or set to `-1` for unlimited. - * * `valid_children` an array of node type strings, that nodes of this type can have as children. Do not specify or set to `-1` for no limits. - * * `icon` a string - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class. Omit to use the default icon from your theme. - * - * There are two predefined types: - * - * * `#` represents the root of the tree, for example `max_children` would control the maximum number of root nodes. - * * `default` represents the default node - any settings here will be applied to all nodes that do not have a type specified. - * - * @name $.jstree.defaults.types - * @plugin types - */ - $.jstree.defaults.types = { - 'default' : {} - }; - $.jstree.defaults.types[$.jstree.root] = {}; - - $.jstree.plugins.types = function (options, parent) { - this.init = function (el, options) { - var i, j; - if(options && options.types && options.types['default']) { - for(i in options.types) { - if(i !== "default" && i !== $.jstree.root && options.types.hasOwnProperty(i)) { - for(j in options.types['default']) { - if(options.types['default'].hasOwnProperty(j) && options.types[i][j] === undefined) { - options.types[i][j] = options.types['default'][j]; - } - } - } - } - } - parent.init.call(this, el, options); - this._model.data[$.jstree.root].type = $.jstree.root; - }; - this.refresh = function (skip_loading, forget_state) { - parent.refresh.call(this, skip_loading, forget_state); - this._model.data[$.jstree.root].type = $.jstree.root; - }; - this.bind = function () { - this.element - .on('model.jstree', $.proxy(function (e, data) { - var m = this._model.data, - dpc = data.nodes, - t = this.settings.types, - i, j, c = 'default'; - for(i = 0, j = dpc.length; i < j; i++) { - c = 'default'; - if(m[dpc[i]].original && m[dpc[i]].original.type && t[m[dpc[i]].original.type]) { - c = m[dpc[i]].original.type; - } - if(m[dpc[i]].data && m[dpc[i]].data.jstree && m[dpc[i]].data.jstree.type && t[m[dpc[i]].data.jstree.type]) { - c = m[dpc[i]].data.jstree.type; - } - m[dpc[i]].type = c; - if(m[dpc[i]].icon === true && t[c].icon !== undefined) { - m[dpc[i]].icon = t[c].icon; - } - } - m[$.jstree.root].type = $.jstree.root; - }, this)); - parent.bind.call(this); - }; - this.get_json = function (obj, options, flat) { - var i, j, - m = this._model.data, - opt = options ? $.extend(true, {}, options, {no_id:false}) : {}, - tmp = parent.get_json.call(this, obj, opt, flat); - if(tmp === false) { return false; } - if($.isArray(tmp)) { - for(i = 0, j = tmp.length; i < j; i++) { - tmp[i].type = tmp[i].id && m[tmp[i].id] && m[tmp[i].id].type ? m[tmp[i].id].type : "default"; - if(options && options.no_id) { - delete tmp[i].id; - if(tmp[i].li_attr && tmp[i].li_attr.id) { - delete tmp[i].li_attr.id; - } - if(tmp[i].a_attr && tmp[i].a_attr.id) { - delete tmp[i].a_attr.id; - } - } - } - } - else { - tmp.type = tmp.id && m[tmp.id] && m[tmp.id].type ? m[tmp.id].type : "default"; - if(options && options.no_id) { - tmp = this._delete_ids(tmp); - } - } - return tmp; - }; - this._delete_ids = function (tmp) { - if($.isArray(tmp)) { - for(var i = 0, j = tmp.length; i < j; i++) { - tmp[i] = this._delete_ids(tmp[i]); - } - return tmp; - } - delete tmp.id; - if(tmp.li_attr && tmp.li_attr.id) { - delete tmp.li_attr.id; - } - if(tmp.a_attr && tmp.a_attr.id) { - delete tmp.a_attr.id; - } - if(tmp.children && $.isArray(tmp.children)) { - tmp.children = this._delete_ids(tmp.children); - } - return tmp; - }; - this.check = function (chk, obj, par, pos, more) { - if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; } - obj = obj && obj.id ? obj : this.get_node(obj); - par = par && par.id ? par : this.get_node(par); - var m = obj && obj.id ? (more && more.origin ? more.origin : $.jstree.reference(obj.id)) : null, tmp, d, i, j; - m = m && m._model && m._model.data ? m._model.data : null; - switch(chk) { - case "create_node": - case "move_node": - case "copy_node": - if(chk !== 'move_node' || $.inArray(obj.id, par.children) === -1) { - tmp = this.get_rules(par); - if(tmp.max_children !== undefined && tmp.max_children !== -1 && tmp.max_children === par.children.length) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_01', 'reason' : 'max_children prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - return false; - } - if(tmp.valid_children !== undefined && tmp.valid_children !== -1 && $.inArray((obj.type || 'default'), tmp.valid_children) === -1) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_02', 'reason' : 'valid_children prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - return false; - } - if(m && obj.children_d && obj.parents) { - d = 0; - for(i = 0, j = obj.children_d.length; i < j; i++) { - d = Math.max(d, m[obj.children_d[i]].parents.length); - } - d = d - obj.parents.length + 1; - } - if(d <= 0 || d === undefined) { d = 1; } - do { - if(tmp.max_depth !== undefined && tmp.max_depth !== -1 && tmp.max_depth < d) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_03', 'reason' : 'max_depth prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - return false; - } - par = this.get_node(par.parent); - tmp = this.get_rules(par); - d++; - } while(par); - } - break; - } - return true; - }; - /** - * used to retrieve the type settings object for a node - * @name get_rules(obj) - * @param {mixed} obj the node to find the rules for - * @return {Object} - * @plugin types - */ - this.get_rules = function (obj) { - obj = this.get_node(obj); - if(!obj) { return false; } - var tmp = this.get_type(obj, true); - if(tmp.max_depth === undefined) { tmp.max_depth = -1; } - if(tmp.max_children === undefined) { tmp.max_children = -1; } - if(tmp.valid_children === undefined) { tmp.valid_children = -1; } - return tmp; - }; - /** - * used to retrieve the type string or settings object for a node - * @name get_type(obj [, rules]) - * @param {mixed} obj the node to find the rules for - * @param {Boolean} rules if set to `true` instead of a string the settings object will be returned - * @return {String|Object} - * @plugin types - */ - this.get_type = function (obj, rules) { - obj = this.get_node(obj); - return (!obj) ? false : ( rules ? $.extend({ 'type' : obj.type }, this.settings.types[obj.type]) : obj.type); - }; - /** - * used to change a node's type - * @name set_type(obj, type) - * @param {mixed} obj the node to change - * @param {String} type the new type - * @plugin types - */ - this.set_type = function (obj, type) { - var t, t1, t2, old_type, old_icon; - if($.isArray(obj)) { - obj = obj.slice(); - for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { - this.set_type(obj[t1], type); - } - return true; - } - t = this.settings.types; - obj = this.get_node(obj); - if(!t[type] || !obj) { return false; } - old_type = obj.type; - old_icon = this.get_icon(obj); - obj.type = type; - if(old_icon === true || (t[old_type] && t[old_type].icon !== undefined && old_icon === t[old_type].icon)) { - this.set_icon(obj, t[type].icon !== undefined ? t[type].icon : true); - } - return true; - }; - }; - // include the types plugin by default - // $.jstree.defaults.plugins.push("types"); - -/** - * ### Unique plugin - * - * Enforces that no nodes with the same name can coexist as siblings. - */ - - /** - * stores all defaults for the unique plugin - * @name $.jstree.defaults.unique - * @plugin unique - */ - $.jstree.defaults.unique = { - /** - * Indicates if the comparison should be case sensitive. Default is `false`. - * @name $.jstree.defaults.unique.case_sensitive - * @plugin unique - */ - case_sensitive : false, - /** - * A callback executed in the instance's scope when a new node is created and the name is already taken, the two arguments are the conflicting name and the counter. The default will produce results like `New node (2)`. - * @name $.jstree.defaults.unique.duplicate - * @plugin unique - */ - duplicate : function (name, counter) { - return name + ' (' + counter + ')'; - } - }; - - $.jstree.plugins.unique = function (options, parent) { - this.check = function (chk, obj, par, pos, more) { - if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; } - obj = obj && obj.id ? obj : this.get_node(obj); - par = par && par.id ? par : this.get_node(par); - if(!par || !par.children) { return true; } - var n = chk === "rename_node" ? pos : obj.text, - c = [], - s = this.settings.unique.case_sensitive, - m = this._model.data, i, j; - for(i = 0, j = par.children.length; i < j; i++) { - c.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase()); - } - if(!s) { n = n.toLowerCase(); } - switch(chk) { - case "delete_node": - return true; - case "rename_node": - i = ($.inArray(n, c) === -1 || (obj.text && obj.text[ s ? 'toString' : 'toLowerCase']() === n)); - if(!i) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_01', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - } - return i; - case "create_node": - i = ($.inArray(n, c) === -1); - if(!i) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_04', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - } - return i; - case "copy_node": - i = ($.inArray(n, c) === -1); - if(!i) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_02', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - } - return i; - case "move_node": - i = ( (obj.parent === par.id && (!more || !more.is_multi)) || $.inArray(n, c) === -1); - if(!i) { - this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_03', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; - } - return i; - } - return true; - }; - this.create_node = function (par, node, pos, callback, is_loaded) { - if(!node || node.text === undefined) { - if(par === null) { - par = $.jstree.root; - } - par = this.get_node(par); - if(!par) { - return parent.create_node.call(this, par, node, pos, callback, is_loaded); - } - pos = pos === undefined ? "last" : pos; - if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { - return parent.create_node.call(this, par, node, pos, callback, is_loaded); - } - if(!node) { node = {}; } - var tmp, n, dpc, i, j, m = this._model.data, s = this.settings.unique.case_sensitive, cb = this.settings.unique.duplicate; - n = tmp = this.get_string('New node'); - dpc = []; - for(i = 0, j = par.children.length; i < j; i++) { - dpc.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase()); - } - i = 1; - while($.inArray(s ? n : n.toLowerCase(), dpc) !== -1) { - n = cb.call(this, tmp, (++i)).toString(); - } - node.text = n; - } - return parent.create_node.call(this, par, node, pos, callback, is_loaded); - }; - }; - - // include the unique plugin by default - // $.jstree.defaults.plugins.push("unique"); - - -/** - * ### Wholerow plugin - * - * Makes each node appear block level. Making selection easier. May cause slow down for large trees in old browsers. - */ - - var div = document.createElement('DIV'); - div.setAttribute('unselectable','on'); - div.setAttribute('role','presentation'); - div.className = 'jstree-wholerow'; - div.innerHTML = ' '; - $.jstree.plugins.wholerow = function (options, parent) { - this.bind = function () { - parent.bind.call(this); - - this.element - .on('ready.jstree set_state.jstree', $.proxy(function () { - this.hide_dots(); - }, this)) - .on("init.jstree loading.jstree ready.jstree", $.proxy(function () { - //div.style.height = this._data.core.li_height + 'px'; - this.get_container_ul().addClass('jstree-wholerow-ul'); - }, this)) - .on("deselect_all.jstree", $.proxy(function (e, data) { - this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked'); - }, this)) - .on("changed.jstree", $.proxy(function (e, data) { - this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked'); - var tmp = false, i, j; - for(i = 0, j = data.selected.length; i < j; i++) { - tmp = this.get_node(data.selected[i], true); - if(tmp && tmp.length) { - tmp.children('.jstree-wholerow').addClass('jstree-wholerow-clicked'); - } - } - }, this)) - .on("open_node.jstree", $.proxy(function (e, data) { - this.get_node(data.node, true).find('.jstree-clicked').parent().children('.jstree-wholerow').addClass('jstree-wholerow-clicked'); - }, this)) - .on("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) { - if(e.type === "hover_node" && this.is_disabled(data.node)) { return; } - this.get_node(data.node, true).children('.jstree-wholerow')[e.type === "hover_node"?"addClass":"removeClass"]('jstree-wholerow-hovered'); - }, this)) - .on("contextmenu.jstree", ".jstree-wholerow", $.proxy(function (e) { - e.preventDefault(); - var tmp = $.Event('contextmenu', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey, pageX : e.pageX, pageY : e.pageY }); - $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp); - }, this)) - /*! - .on("mousedown.jstree touchstart.jstree", ".jstree-wholerow", function (e) { - if(e.target === e.currentTarget) { - var a = $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor"); - e.target = a[0]; - a.trigger(e); - } - }) - */ - .on("click.jstree", ".jstree-wholerow", function (e) { - e.stopImmediatePropagation(); - var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey }); - $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus(); - }) - .on("click.jstree", ".jstree-leaf > .jstree-ocl", $.proxy(function (e) { - e.stopImmediatePropagation(); - var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey }); - $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus(); - }, this)) - .on("mouseover.jstree", ".jstree-wholerow, .jstree-icon", $.proxy(function (e) { - e.stopImmediatePropagation(); - if(!this.is_disabled(e.currentTarget)) { - this.hover_node(e.currentTarget); - } - return false; - }, this)) - .on("mouseleave.jstree", ".jstree-node", $.proxy(function (e) { - this.dehover_node(e.currentTarget); - }, this)); - }; - this.teardown = function () { - if(this.settings.wholerow) { - this.element.find(".jstree-wholerow").remove(); - } - parent.teardown.call(this); - }; - this.redraw_node = function(obj, deep, callback, force_render) { - obj = parent.redraw_node.apply(this, arguments); - if(obj) { - var tmp = div.cloneNode(true); - //tmp.style.height = this._data.core.li_height + 'px'; - if($.inArray(obj.id, this._data.core.selected) !== -1) { tmp.className += ' jstree-wholerow-clicked'; } - if(this._data.core.focused && this._data.core.focused === obj.id) { tmp.className += ' jstree-wholerow-hovered'; } - obj.insertBefore(tmp, obj.childNodes[0]); - } - return obj; - }; - }; - // include the wholerow plugin by default - // $.jstree.defaults.plugins.push("wholerow"); - if(document.registerElement && Object && Object.create) { - var proto = Object.create(HTMLElement.prototype); - proto.createdCallback = function () { - var c = { core : {}, plugins : [] }, i; - for(i in $.jstree.plugins) { - if($.jstree.plugins.hasOwnProperty(i) && this.attributes[i]) { - c.plugins.push(i); - if(this.getAttribute(i) && JSON.parse(this.getAttribute(i))) { - c[i] = JSON.parse(this.getAttribute(i)); - } - } - } - for(i in $.jstree.defaults.core) { - if($.jstree.defaults.core.hasOwnProperty(i) && this.attributes[i]) { - c.core[i] = JSON.parse(this.getAttribute(i)) || this.getAttribute(i); - } - } - $(this).jstree(c); - }; - // proto.attributeChangedCallback = function (name, previous, value) { }; - try { - document.registerElement("vakata-jstree", { prototype: proto }); - } catch(ignore) { } - } - -})); \ No newline at end of file diff --git a/frontend/vendor/assets/stylesheets/jstree/32px.png b/frontend/vendor/assets/stylesheets/jstree/32px.png deleted file mode 100755 index 1532715248..0000000000 Binary files a/frontend/vendor/assets/stylesheets/jstree/32px.png and /dev/null differ diff --git a/frontend/vendor/assets/stylesheets/jstree/40px.png b/frontend/vendor/assets/stylesheets/jstree/40px.png deleted file mode 100755 index 1959347aea..0000000000 Binary files a/frontend/vendor/assets/stylesheets/jstree/40px.png and /dev/null differ diff --git a/frontend/vendor/assets/stylesheets/jstree/d.gif b/frontend/vendor/assets/stylesheets/jstree/d.gif deleted file mode 100755 index 6eb0004ce3..0000000000 Binary files a/frontend/vendor/assets/stylesheets/jstree/d.gif and /dev/null differ diff --git a/frontend/vendor/assets/stylesheets/jstree/d.png b/frontend/vendor/assets/stylesheets/jstree/d.png deleted file mode 100755 index 275daeca2d..0000000000 Binary files a/frontend/vendor/assets/stylesheets/jstree/d.png and /dev/null differ diff --git a/frontend/vendor/assets/stylesheets/jstree/dot_for_ie.gif b/frontend/vendor/assets/stylesheets/jstree/dot_for_ie.gif deleted file mode 100755 index c0cc5fda7c..0000000000 Binary files a/frontend/vendor/assets/stylesheets/jstree/dot_for_ie.gif and /dev/null differ diff --git a/frontend/vendor/assets/stylesheets/jstree/style.css.less b/frontend/vendor/assets/stylesheets/jstree/style.css.less deleted file mode 100755 index 85bd0d8d31..0000000000 --- a/frontend/vendor/assets/stylesheets/jstree/style.css.less +++ /dev/null @@ -1,1061 +0,0 @@ -/* jsTree default theme */ -.jstree-node, -.jstree-children, -.jstree-container-ul { - display: block; - margin: 0; - padding: 0; - list-style-type: none; - list-style-image: none; -} -.jstree-node { - white-space: nowrap; -} -.jstree-anchor { - display: inline-block; - color: black; - white-space: nowrap; - padding: 0 4px 0 1px; - margin: 0; - vertical-align: top; -} -.jstree-anchor:focus { - outline: 0; -} -.jstree-anchor, -.jstree-anchor:link, -.jstree-anchor:visited, -.jstree-anchor:hover, -.jstree-anchor:active { - text-decoration: none; - color: inherit; -} -.jstree-icon { - display: inline-block; - text-decoration: none; - margin: 0; - padding: 0; - vertical-align: top; - text-align: center; -} -.jstree-icon:empty { - display: inline-block; - text-decoration: none; - margin: 0; - padding: 0; - vertical-align: top; - text-align: center; -} -.jstree-ocl { - cursor: pointer; -} -.jstree-leaf > .jstree-ocl { - cursor: default; -} -.jstree .jstree-open > .jstree-children { - display: block; -} -.jstree .jstree-closed > .jstree-children, -.jstree .jstree-leaf > .jstree-children { - display: none; -} -.jstree-anchor > .jstree-themeicon { - margin-right: 2px; -} -.jstree-no-icons .jstree-themeicon, -.jstree-anchor > .jstree-themeicon-hidden { - display: none; -} -.jstree-hidden { - display: none; -} -.jstree-rtl .jstree-anchor { - padding: 0 1px 0 4px; -} -.jstree-rtl .jstree-anchor > .jstree-themeicon { - margin-left: 2px; - margin-right: 0; -} -.jstree-rtl .jstree-node { - margin-left: 0; -} -.jstree-rtl .jstree-container-ul > .jstree-node { - margin-right: 0; -} -.jstree-wholerow-ul { - position: relative; - display: inline-block; - min-width: 100%; -} -.jstree-wholerow-ul .jstree-leaf > .jstree-ocl { - cursor: pointer; -} -.jstree-wholerow-ul .jstree-anchor, -.jstree-wholerow-ul .jstree-icon { - position: relative; -} -.jstree-wholerow-ul .jstree-wholerow { - width: 100%; - cursor: pointer; - position: absolute; - left: 0; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.vakata-context { - display: none; -} -.vakata-context, -.vakata-context ul { - margin: 0; - padding: 2px; - position: absolute; - background: #f5f5f5; - border: 1px solid #979797; - box-shadow: 2px 2px 2px #999999; -} -.vakata-context ul { - list-style: none; - left: 100%; - margin-top: -2.7em; - margin-left: -4px; -} -.vakata-context .vakata-context-right ul { - left: auto; - right: 100%; - margin-left: auto; - margin-right: -4px; -} -.vakata-context li { - list-style: none; - display: inline; -} -.vakata-context li > a { - display: block; - padding: 0 2em 0 2em; - text-decoration: none; - width: auto; - color: black; - white-space: nowrap; - line-height: 2.4em; - text-shadow: 1px 1px 0 white; - border-radius: 1px; -} -.vakata-context li > a:hover { - position: relative; - background-color: #e8eff7; - box-shadow: 0 0 2px #0a6aa1; -} -.vakata-context li > a.vakata-context-parent { - background-image: url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAIORI4JlrqN1oMSnmmZDQUAOw=="); - background-position: right center; - background-repeat: no-repeat; -} -.vakata-context li > a:focus { - outline: 0; -} -.vakata-context .vakata-context-hover > a { - position: relative; - background-color: #e8eff7; - box-shadow: 0 0 2px #0a6aa1; -} -.vakata-context .vakata-context-separator > a, -.vakata-context .vakata-context-separator > a:hover { - background: white; - border: 0; - border-top: 1px solid #e2e3e3; - height: 1px; - min-height: 1px; - max-height: 1px; - padding: 0; - margin: 0 0 0 2.4em; - border-left: 1px solid #e0e0e0; - text-shadow: 0 0 0 transparent; - box-shadow: 0 0 0 transparent; - border-radius: 0; -} -.vakata-context .vakata-contextmenu-disabled a, -.vakata-context .vakata-contextmenu-disabled a:hover { - color: silver; - background-color: transparent; - border: 0; - box-shadow: 0 0 0; -} -.vakata-context li > a > i { - text-decoration: none; - display: inline-block; - width: 2.4em; - height: 2.4em; - background: transparent; - margin: 0 0 0 -2em; - vertical-align: top; - text-align: center; - line-height: 2.4em; -} -.vakata-context li > a > i:empty { - width: 2.4em; - line-height: 2.4em; -} -.vakata-context li > a .vakata-contextmenu-sep { - display: inline-block; - width: 1px; - height: 2.4em; - background: white; - margin: 0 0.5em 0 0; - border-left: 1px solid #e2e3e3; -} -.vakata-context .vakata-contextmenu-shortcut { - font-size: 0.8em; - color: silver; - opacity: 0.5; - display: none; -} -.vakata-context-rtl ul { - left: auto; - right: 100%; - margin-left: auto; - margin-right: -4px; -} -.vakata-context-rtl li > a.vakata-context-parent { - background-image: url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAINjI+AC7rWHIsPtmoxLAA7"); - background-position: left center; - background-repeat: no-repeat; -} -.vakata-context-rtl .vakata-context-separator > a { - margin: 0 2.4em 0 0; - border-left: 0; - border-right: 1px solid #e2e3e3; -} -.vakata-context-rtl .vakata-context-left ul { - right: auto; - left: 100%; - margin-left: -4px; - margin-right: auto; -} -.vakata-context-rtl li > a > i { - margin: 0 -2em 0 0; -} -.vakata-context-rtl li > a .vakata-contextmenu-sep { - margin: 0 0 0 0.5em; - border-left-color: white; - background: #e2e3e3; -} -#jstree-marker { - position: absolute; - top: 0; - left: 0; - margin: -5px 0 0 0; - padding: 0; - border-right: 0; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid; - width: 0; - height: 0; - font-size: 0; - line-height: 0; -} -#jstree-dnd { - line-height: 16px; - margin: 0; - padding: 4px; -} -#jstree-dnd .jstree-icon, -#jstree-dnd .jstree-copy { - display: inline-block; - text-decoration: none; - margin: 0 2px 0 0; - padding: 0; - width: 16px; - height: 16px; -} -#jstree-dnd .jstree-ok { - background: green; -} -#jstree-dnd .jstree-er { - background: red; -} -#jstree-dnd .jstree-copy { - margin: 0 2px 0 2px; -} -.jstree-default .jstree-node, -.jstree-default .jstree-icon { - background-repeat: no-repeat; - background-color: transparent; -} -.jstree-default .jstree-anchor, -.jstree-default .jstree-wholerow { - transition: background-color 0.15s, box-shadow 0.15s; -} -.jstree-default .jstree-hovered { - background: #e7f4f9; - border-radius: 2px; - box-shadow: inset 0 0 1px #cccccc; -} -.jstree-default .jstree-clicked { - background: #beebff; - border-radius: 2px; - box-shadow: inset 0 0 1px #999999; -} -.jstree-default .jstree-no-icons .jstree-anchor > .jstree-themeicon { - display: none; -} -.jstree-default .jstree-disabled { - background: transparent; - color: #666666; -} -.jstree-default .jstree-disabled.jstree-hovered { - background: transparent; - box-shadow: none; -} -.jstree-default .jstree-disabled.jstree-clicked { - background: #efefef; -} -.jstree-default .jstree-disabled > .jstree-icon { - opacity: 0.8; - filter: url("data:image/svg+xml;utf8,#jstree-grayscale"); - /* Firefox 10+ */ - filter: gray; - /* IE6-9 */ - -webkit-filter: grayscale(100%); - /* Chrome 19+ & Safari 6+ */ -} -.jstree-default .jstree-search { - font-style: italic; - color: #8b0000; - font-weight: bold; -} -.jstree-default .jstree-no-checkboxes .jstree-checkbox { - display: none !important; -} -.jstree-default.jstree-checkbox-no-clicked .jstree-clicked { - background: transparent; - box-shadow: none; -} -.jstree-default.jstree-checkbox-no-clicked .jstree-clicked.jstree-hovered { - background: #e7f4f9; -} -.jstree-default.jstree-checkbox-no-clicked > .jstree-wholerow-ul .jstree-wholerow-clicked { - background: transparent; -} -.jstree-default.jstree-checkbox-no-clicked > .jstree-wholerow-ul .jstree-wholerow-clicked.jstree-wholerow-hovered { - background: #e7f4f9; -} -.jstree-default > .jstree-striped { - min-width: 100%; - display: inline-block; - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB/qqA+AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMNAMM9s3UAAAAXSURBVHjajcEBAQAAAIKg/H/aCQZ70AUBjAATb6YPDgAAAABJRU5ErkJggg==") left top repeat; -} -.jstree-default > .jstree-wholerow-ul .jstree-hovered, -.jstree-default > .jstree-wholerow-ul .jstree-clicked { - background: transparent; - box-shadow: none; - border-radius: 0; -} -.jstree-default .jstree-wholerow { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} -.jstree-default .jstree-wholerow-hovered { - background: #e7f4f9; -} -.jstree-default .jstree-wholerow-clicked { - background: #beebff; - background: -webkit-linear-gradient(top, #beebff 0%, #a8e4ff 100%); - background: linear-gradient(to bottom, #beebff 0%, #a8e4ff 100%); -} -.jstree-default .jstree-node { - min-height: 24px; - line-height: 24px; - margin-left: 24px; - min-width: 24px; -} -.jstree-default .jstree-anchor { - line-height: 24px; - height: 24px; -} -.jstree-default .jstree-icon { - width: 24px; - height: 24px; - line-height: 24px; -} -.jstree-default .jstree-icon:empty { - width: 24px; - height: 24px; - line-height: 24px; -} -.jstree-default.jstree-rtl .jstree-node { - margin-right: 24px; -} -.jstree-default .jstree-wholerow { - height: 24px; -} -.jstree-default .jstree-node, -.jstree-default .jstree-icon { - background-image: url("32px.png"); -} -.jstree-default .jstree-node { - background-position: -292px -4px; - background-repeat: repeat-y; -} -.jstree-default .jstree-last { - background: transparent; -} -.jstree-default .jstree-open > .jstree-ocl { - background-position: -132px -4px; -} -.jstree-default .jstree-closed > .jstree-ocl { - background-position: -100px -4px; -} -.jstree-default .jstree-leaf > .jstree-ocl { - background-position: -68px -4px; -} -.jstree-default .jstree-themeicon { - background-position: -260px -4px; -} -.jstree-default > .jstree-no-dots .jstree-node, -.jstree-default > .jstree-no-dots .jstree-leaf > .jstree-ocl { - background: transparent; -} -.jstree-default > .jstree-no-dots .jstree-open > .jstree-ocl { - background-position: -36px -4px; -} -.jstree-default > .jstree-no-dots .jstree-closed > .jstree-ocl { - background-position: -4px -4px; -} -.jstree-default .jstree-disabled { - background: transparent; -} -.jstree-default .jstree-disabled.jstree-hovered { - background: transparent; -} -.jstree-default .jstree-disabled.jstree-clicked { - background: #efefef; -} -.jstree-default .jstree-checkbox { - background-position: -164px -4px; -} -.jstree-default .jstree-checkbox:hover { - background-position: -164px -36px; -} -.jstree-default.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox, -.jstree-default .jstree-checked > .jstree-checkbox { - background-position: -228px -4px; -} -.jstree-default.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox:hover, -.jstree-default .jstree-checked > .jstree-checkbox:hover { - background-position: -228px -36px; -} -.jstree-default .jstree-anchor > .jstree-undetermined { - background-position: -196px -4px; -} -.jstree-default .jstree-anchor > .jstree-undetermined:hover { - background-position: -196px -36px; -} -.jstree-default .jstree-checkbox-disabled { - opacity: 0.8; - filter: url("data:image/svg+xml;utf8,#jstree-grayscale"); - /* Firefox 10+ */ - filter: gray; - /* IE6-9 */ - -webkit-filter: grayscale(100%); - /* Chrome 19+ & Safari 6+ */ -} -.jstree-default > .jstree-striped { - background-size: auto 48px; -} -.jstree-default.jstree-rtl .jstree-node { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); - background-position: 100% 1px; - background-repeat: repeat-y; -} -.jstree-default.jstree-rtl .jstree-last { - background: transparent; -} -.jstree-default.jstree-rtl .jstree-open > .jstree-ocl { - background-position: -132px -36px; -} -.jstree-default.jstree-rtl .jstree-closed > .jstree-ocl { - background-position: -100px -36px; -} -.jstree-default.jstree-rtl .jstree-leaf > .jstree-ocl { - background-position: -68px -36px; -} -.jstree-default.jstree-rtl > .jstree-no-dots .jstree-node, -.jstree-default.jstree-rtl > .jstree-no-dots .jstree-leaf > .jstree-ocl { - background: transparent; -} -.jstree-default.jstree-rtl > .jstree-no-dots .jstree-open > .jstree-ocl { - background-position: -36px -36px; -} -.jstree-default.jstree-rtl > .jstree-no-dots .jstree-closed > .jstree-ocl { - background-position: -4px -36px; -} -.jstree-default .jstree-themeicon-custom { - background-color: transparent; - background-image: none; - background-position: 0 0; -} -.jstree-default > .jstree-container-ul .jstree-loading > .jstree-ocl { - background: url("throbber.gif") center center no-repeat; -} -.jstree-default .jstree-file { - background: url("32px.png") -100px -68px no-repeat; -} -.jstree-default .jstree-folder { - background: url("32px.png") -260px -4px no-repeat; -} -.jstree-default > .jstree-container-ul > .jstree-node { - margin-left: 0; - margin-right: 0; -} -#jstree-dnd.jstree-default { - line-height: 24px; - padding: 0 4px; -} -#jstree-dnd.jstree-default .jstree-ok, -#jstree-dnd.jstree-default .jstree-er { - background-image: url("32px.png"); - background-repeat: no-repeat; - background-color: transparent; -} -#jstree-dnd.jstree-default i { - background: transparent; - width: 24px; - height: 24px; - line-height: 24px; -} -#jstree-dnd.jstree-default .jstree-ok { - background-position: -4px -68px; -} -#jstree-dnd.jstree-default .jstree-er { - background-position: -36px -68px; -} -.jstree-default.jstree-rtl .jstree-node { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); -} -.jstree-default.jstree-rtl .jstree-last { - background: transparent; -} -.jstree-default-small .jstree-node { - min-height: 18px; - line-height: 18px; - margin-left: 18px; - min-width: 18px; -} -.jstree-default-small .jstree-anchor { - line-height: 18px; - height: 18px; -} -.jstree-default-small .jstree-icon { - width: 18px; - height: 18px; - line-height: 18px; -} -.jstree-default-small .jstree-icon:empty { - width: 18px; - height: 18px; - line-height: 18px; -} -.jstree-default-small.jstree-rtl .jstree-node { - margin-right: 18px; -} -.jstree-default-small .jstree-wholerow { - height: 18px; -} -.jstree-default-small .jstree-node, -.jstree-default-small .jstree-icon { - background-image: url("32px.png"); -} -.jstree-default-small .jstree-node { - background-position: -295px -7px; - background-repeat: repeat-y; -} -.jstree-default-small .jstree-last { - background: transparent; -} -.jstree-default-small .jstree-open > .jstree-ocl { - background-position: -135px -7px; -} -.jstree-default-small .jstree-closed > .jstree-ocl { - background-position: -103px -7px; -} -.jstree-default-small .jstree-leaf > .jstree-ocl { - background-position: -71px -7px; -} -.jstree-default-small .jstree-themeicon { - background-position: -263px -7px; -} -.jstree-default-small > .jstree-no-dots .jstree-node, -.jstree-default-small > .jstree-no-dots .jstree-leaf > .jstree-ocl { - background: transparent; -} -.jstree-default-small > .jstree-no-dots .jstree-open > .jstree-ocl { - background-position: -39px -7px; -} -.jstree-default-small > .jstree-no-dots .jstree-closed > .jstree-ocl { - background-position: -7px -7px; -} -.jstree-default-small .jstree-disabled { - background: transparent; -} -.jstree-default-small .jstree-disabled.jstree-hovered { - background: transparent; -} -.jstree-default-small .jstree-disabled.jstree-clicked { - background: #efefef; -} -.jstree-default-small .jstree-checkbox { - background-position: -167px -7px; -} -.jstree-default-small .jstree-checkbox:hover { - background-position: -167px -39px; -} -.jstree-default-small.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox, -.jstree-default-small .jstree-checked > .jstree-checkbox { - background-position: -231px -7px; -} -.jstree-default-small.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox:hover, -.jstree-default-small .jstree-checked > .jstree-checkbox:hover { - background-position: -231px -39px; -} -.jstree-default-small .jstree-anchor > .jstree-undetermined { - background-position: -199px -7px; -} -.jstree-default-small .jstree-anchor > .jstree-undetermined:hover { - background-position: -199px -39px; -} -.jstree-default-small .jstree-checkbox-disabled { - opacity: 0.8; - filter: url("data:image/svg+xml;utf8,#jstree-grayscale"); - /* Firefox 10+ */ - filter: gray; - /* IE6-9 */ - -webkit-filter: grayscale(100%); - /* Chrome 19+ & Safari 6+ */ -} -.jstree-default-small > .jstree-striped { - background-size: auto 36px; -} -.jstree-default-small.jstree-rtl .jstree-node { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); - background-position: 100% 1px; - background-repeat: repeat-y; -} -.jstree-default-small.jstree-rtl .jstree-last { - background: transparent; -} -.jstree-default-small.jstree-rtl .jstree-open > .jstree-ocl { - background-position: -135px -39px; -} -.jstree-default-small.jstree-rtl .jstree-closed > .jstree-ocl { - background-position: -103px -39px; -} -.jstree-default-small.jstree-rtl .jstree-leaf > .jstree-ocl { - background-position: -71px -39px; -} -.jstree-default-small.jstree-rtl > .jstree-no-dots .jstree-node, -.jstree-default-small.jstree-rtl > .jstree-no-dots .jstree-leaf > .jstree-ocl { - background: transparent; -} -.jstree-default-small.jstree-rtl > .jstree-no-dots .jstree-open > .jstree-ocl { - background-position: -39px -39px; -} -.jstree-default-small.jstree-rtl > .jstree-no-dots .jstree-closed > .jstree-ocl { - background-position: -7px -39px; -} -.jstree-default-small .jstree-themeicon-custom { - background-color: transparent; - background-image: none; - background-position: 0 0; -} -.jstree-default-small > .jstree-container-ul .jstree-loading > .jstree-ocl { - background: url("throbber.gif") center center no-repeat; -} -.jstree-default-small .jstree-file { - background: url("32px.png") -103px -71px no-repeat; -} -.jstree-default-small .jstree-folder { - background: url("32px.png") -263px -7px no-repeat; -} -.jstree-default-small > .jstree-container-ul > .jstree-node { - margin-left: 0; - margin-right: 0; -} -#jstree-dnd.jstree-default-small { - line-height: 18px; - padding: 0 4px; -} -#jstree-dnd.jstree-default-small .jstree-ok, -#jstree-dnd.jstree-default-small .jstree-er { - background-image: url("32px.png"); - background-repeat: no-repeat; - background-color: transparent; -} -#jstree-dnd.jstree-default-small i { - background: transparent; - width: 18px; - height: 18px; - line-height: 18px; -} -#jstree-dnd.jstree-default-small .jstree-ok { - background-position: -7px -71px; -} -#jstree-dnd.jstree-default-small .jstree-er { - background-position: -39px -71px; -} -.jstree-default-small.jstree-rtl .jstree-node { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg=="); -} -.jstree-default-small.jstree-rtl .jstree-last { - background: transparent; -} -.jstree-default-large .jstree-node { - min-height: 32px; - line-height: 32px; - margin-left: 32px; - min-width: 32px; -} -.jstree-default-large .jstree-anchor { - line-height: 32px; - height: 32px; -} -.jstree-default-large .jstree-icon { - width: 32px; - height: 32px; - line-height: 32px; -} -.jstree-default-large .jstree-icon:empty { - width: 32px; - height: 32px; - line-height: 32px; -} -.jstree-default-large.jstree-rtl .jstree-node { - margin-right: 32px; -} -.jstree-default-large .jstree-wholerow { - height: 32px; -} -.jstree-default-large .jstree-node, -.jstree-default-large .jstree-icon { - background-image: url("32px.png"); -} -.jstree-default-large .jstree-node { - background-position: -288px 0px; - background-repeat: repeat-y; -} -.jstree-default-large .jstree-last { - background: transparent; -} -.jstree-default-large .jstree-open > .jstree-ocl { - background-position: -128px 0px; -} -.jstree-default-large .jstree-closed > .jstree-ocl { - background-position: -96px 0px; -} -.jstree-default-large .jstree-leaf > .jstree-ocl { - background-position: -64px 0px; -} -.jstree-default-large .jstree-themeicon { - background-position: -256px 0px; -} -.jstree-default-large > .jstree-no-dots .jstree-node, -.jstree-default-large > .jstree-no-dots .jstree-leaf > .jstree-ocl { - background: transparent; -} -.jstree-default-large > .jstree-no-dots .jstree-open > .jstree-ocl { - background-position: -32px 0px; -} -.jstree-default-large > .jstree-no-dots .jstree-closed > .jstree-ocl { - background-position: 0px 0px; -} -.jstree-default-large .jstree-disabled { - background: transparent; -} -.jstree-default-large .jstree-disabled.jstree-hovered { - background: transparent; -} -.jstree-default-large .jstree-disabled.jstree-clicked { - background: #efefef; -} -.jstree-default-large .jstree-checkbox { - background-position: -160px 0px; -} -.jstree-default-large .jstree-checkbox:hover { - background-position: -160px -32px; -} -.jstree-default-large.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox, -.jstree-default-large .jstree-checked > .jstree-checkbox { - background-position: -224px 0px; -} -.jstree-default-large.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox:hover, -.jstree-default-large .jstree-checked > .jstree-checkbox:hover { - background-position: -224px -32px; -} -.jstree-default-large .jstree-anchor > .jstree-undetermined { - background-position: -192px 0px; -} -.jstree-default-large .jstree-anchor > .jstree-undetermined:hover { - background-position: -192px -32px; -} -.jstree-default-large .jstree-checkbox-disabled { - opacity: 0.8; - filter: url("data:image/svg+xml;utf8,#jstree-grayscale"); - /* Firefox 10+ */ - filter: gray; - /* IE6-9 */ - -webkit-filter: grayscale(100%); - /* Chrome 19+ & Safari 6+ */ -} -.jstree-default-large > .jstree-striped { - background-size: auto 64px; -} -.jstree-default-large.jstree-rtl .jstree-node { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); - background-position: 100% 1px; - background-repeat: repeat-y; -} -.jstree-default-large.jstree-rtl .jstree-last { - background: transparent; -} -.jstree-default-large.jstree-rtl .jstree-open > .jstree-ocl { - background-position: -128px -32px; -} -.jstree-default-large.jstree-rtl .jstree-closed > .jstree-ocl { - background-position: -96px -32px; -} -.jstree-default-large.jstree-rtl .jstree-leaf > .jstree-ocl { - background-position: -64px -32px; -} -.jstree-default-large.jstree-rtl > .jstree-no-dots .jstree-node, -.jstree-default-large.jstree-rtl > .jstree-no-dots .jstree-leaf > .jstree-ocl { - background: transparent; -} -.jstree-default-large.jstree-rtl > .jstree-no-dots .jstree-open > .jstree-ocl { - background-position: -32px -32px; -} -.jstree-default-large.jstree-rtl > .jstree-no-dots .jstree-closed > .jstree-ocl { - background-position: 0px -32px; -} -.jstree-default-large .jstree-themeicon-custom { - background-color: transparent; - background-image: none; - background-position: 0 0; -} -.jstree-default-large > .jstree-container-ul .jstree-loading > .jstree-ocl { - background: url("throbber.gif") center center no-repeat; -} -.jstree-default-large .jstree-file { - background: url("32px.png") -96px -64px no-repeat; -} -.jstree-default-large .jstree-folder { - background: url("32px.png") -256px 0px no-repeat; -} -.jstree-default-large > .jstree-container-ul > .jstree-node { - margin-left: 0; - margin-right: 0; -} -#jstree-dnd.jstree-default-large { - line-height: 32px; - padding: 0 4px; -} -#jstree-dnd.jstree-default-large .jstree-ok, -#jstree-dnd.jstree-default-large .jstree-er { - background-image: url("32px.png"); - background-repeat: no-repeat; - background-color: transparent; -} -#jstree-dnd.jstree-default-large i { - background: transparent; - width: 32px; - height: 32px; - line-height: 32px; -} -#jstree-dnd.jstree-default-large .jstree-ok { - background-position: 0px -64px; -} -#jstree-dnd.jstree-default-large .jstree-er { - background-position: -32px -64px; -} -.jstree-default-large.jstree-rtl .jstree-node { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg=="); -} -.jstree-default-large.jstree-rtl .jstree-last { - background: transparent; -} -@media (max-width: 768px) { - #jstree-dnd.jstree-dnd-responsive { - line-height: 40px; - font-weight: bold; - font-size: 1.1em; - text-shadow: 1px 1px white; - } - #jstree-dnd.jstree-dnd-responsive > i { - background: transparent; - width: 40px; - height: 40px; - } - #jstree-dnd.jstree-dnd-responsive > .jstree-ok { - background-image: url("40px.png"); - background-position: 0 -200px; - background-size: 120px 240px; - } - #jstree-dnd.jstree-dnd-responsive > .jstree-er { - background-image: url("40px.png"); - background-position: -40px -200px; - background-size: 120px 240px; - } - #jstree-marker.jstree-dnd-responsive { - border-left-width: 10px; - border-top-width: 10px; - border-bottom-width: 10px; - margin-top: -10px; - } -} -@media (max-width: 768px) { - .jstree-default-responsive { - /* - .jstree-open > .jstree-ocl, - .jstree-closed > .jstree-ocl { border-radius:20px; background-color:white; } - */ - } - .jstree-default-responsive .jstree-icon { - background-image: url("40px.png"); - } - .jstree-default-responsive .jstree-node, - .jstree-default-responsive .jstree-leaf > .jstree-ocl { - background: transparent; - } - .jstree-default-responsive .jstree-node { - min-height: 40px; - line-height: 40px; - margin-left: 40px; - min-width: 40px; - white-space: nowrap; - } - .jstree-default-responsive .jstree-anchor { - line-height: 40px; - height: 40px; - } - .jstree-default-responsive .jstree-icon, - .jstree-default-responsive .jstree-icon:empty { - width: 40px; - height: 40px; - line-height: 40px; - } - .jstree-default-responsive > .jstree-container-ul > .jstree-node { - margin-left: 0; - } - .jstree-default-responsive.jstree-rtl .jstree-node { - margin-left: 0; - margin-right: 40px; - } - .jstree-default-responsive.jstree-rtl .jstree-container-ul > .jstree-node { - margin-right: 0; - } - .jstree-default-responsive .jstree-ocl, - .jstree-default-responsive .jstree-themeicon, - .jstree-default-responsive .jstree-checkbox { - background-size: 120px 240px; - } - .jstree-default-responsive .jstree-leaf > .jstree-ocl { - background: transparent; - } - .jstree-default-responsive .jstree-open > .jstree-ocl { - background-position: 0 0px !important; - } - .jstree-default-responsive .jstree-closed > .jstree-ocl { - background-position: 0 -40px !important; - } - .jstree-default-responsive.jstree-rtl .jstree-closed > .jstree-ocl { - background-position: -40px 0px !important; - } - .jstree-default-responsive .jstree-themeicon { - background-position: -40px -40px; - } - .jstree-default-responsive .jstree-checkbox, - .jstree-default-responsive .jstree-checkbox:hover { - background-position: -40px -80px; - } - .jstree-default-responsive.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox, - .jstree-default-responsive.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox:hover, - .jstree-default-responsive .jstree-checked > .jstree-checkbox, - .jstree-default-responsive .jstree-checked > .jstree-checkbox:hover { - background-position: 0 -80px; - } - .jstree-default-responsive .jstree-anchor > .jstree-undetermined, - .jstree-default-responsive .jstree-anchor > .jstree-undetermined:hover { - background-position: 0 -120px; - } - .jstree-default-responsive .jstree-anchor { - font-weight: bold; - font-size: 1.1em; - text-shadow: 1px 1px white; - } - .jstree-default-responsive > .jstree-striped { - background: transparent; - } - .jstree-default-responsive .jstree-wholerow { - border-top: 1px solid rgba(255, 255, 255, 0.7); - border-bottom: 1px solid rgba(64, 64, 64, 0.2); - background: #ebebeb; - height: 40px; - } - .jstree-default-responsive .jstree-wholerow-hovered { - background: #e7f4f9; - } - .jstree-default-responsive .jstree-wholerow-clicked { - background: #beebff; - } - .jstree-default-responsive .jstree-children .jstree-last > .jstree-wholerow { - box-shadow: inset 0 -6px 3px -5px #666666; - } - .jstree-default-responsive .jstree-children .jstree-open > .jstree-wholerow { - box-shadow: inset 0 6px 3px -5px #666666; - border-top: 0; - } - .jstree-default-responsive .jstree-children .jstree-open + .jstree-open { - box-shadow: none; - } - .jstree-default-responsive .jstree-node, - .jstree-default-responsive .jstree-icon, - .jstree-default-responsive .jstree-node > .jstree-ocl, - .jstree-default-responsive .jstree-themeicon, - .jstree-default-responsive .jstree-checkbox { - background-image: url("40px.png"); - background-size: 120px 240px; - } - .jstree-default-responsive .jstree-node { - background-position: -80px 0; - background-repeat: repeat-y; - } - .jstree-default-responsive .jstree-last { - background: transparent; - } - .jstree-default-responsive .jstree-leaf > .jstree-ocl { - background-position: -40px -120px; - } - .jstree-default-responsive .jstree-last > .jstree-ocl { - background-position: -40px -160px; - } - .jstree-default-responsive .jstree-themeicon-custom { - background-color: transparent; - background-image: none; - background-position: 0 0; - } - .jstree-default-responsive .jstree-file { - background: url("40px.png") 0 -160px no-repeat; - background-size: 120px 240px; - } - .jstree-default-responsive .jstree-folder { - background: url("40px.png") -40px -40px no-repeat; - background-size: 120px 240px; - } - .jstree-default-responsive > .jstree-container-ul > .jstree-node { - margin-left: 0; - margin-right: 0; - } -} diff --git a/frontend/vendor/assets/stylesheets/jstree/throbber.gif b/frontend/vendor/assets/stylesheets/jstree/throbber.gif deleted file mode 100644 index 5b33f7e54f..0000000000 Binary files a/frontend/vendor/assets/stylesheets/jstree/throbber.gif and /dev/null differ diff --git a/indexer/Gemfile b/indexer/Gemfile index d95ae8b652..9a31dd7c4b 100644 --- a/indexer/Gemfile +++ b/indexer/Gemfile @@ -1,6 +1,5 @@ source "https://rubygems.org" gem "atomic", '= 1.0.1' -gem 'rack', '1.4.7' gem "json", "1.8.0" -gem "sinatra", '1.3.6', :require => false +gem "sinatra", '1.4.7', :require => false diff --git a/indexer/Gemfile.lock b/indexer/Gemfile.lock index bf3599a1bd..5437d30d15 100644 --- a/indexer/Gemfile.lock +++ b/indexer/Gemfile.lock @@ -3,14 +3,14 @@ GEM specs: atomic (1.0.1-java) json (1.8.0-java) - rack (1.4.7) + rack (1.6.5) rack-protection (1.5.3) rack - sinatra (1.3.6) - rack (~> 1.4) - rack-protection (~> 1.3) - tilt (~> 1.3, >= 1.3.3) - tilt (1.4.1) + sinatra (1.4.7) + rack (~> 1.5) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + tilt (2.0.6) PLATFORMS java @@ -18,8 +18,7 @@ PLATFORMS DEPENDENCIES atomic (= 1.0.1) json (= 1.8.0) - rack (= 1.4.7) - sinatra (= 1.3.6) + sinatra (= 1.4.7) BUNDLED WITH 1.12.5 diff --git a/indexer/app/lib/index_batch.rb b/indexer/app/lib/index_batch.rb index 36ec65b95e..f903fe7482 100644 --- a/indexer/app/lib/index_batch.rb +++ b/indexer/app/lib/index_batch.rb @@ -14,6 +14,10 @@ def initialize @closed = false @filestore = ASUtils.tempfile('index_batch') + + # Don't mess up our line breaks under Windows! + @filestore.binmode + self.write("[\n") end @@ -78,7 +82,9 @@ def each(&block) def to_json_stream self.close @filestore.close - File.open(@filestore.path, "r") + + # Open with "b" to avoid converting \n to \r\n on Windows + File.open(@filestore.path, "rb") end diff --git a/indexer/app/lib/indexer_common.rb b/indexer/app/lib/indexer_common.rb index 89439deb37..1b192e1022 100644 --- a/indexer/app/lib/indexer_common.rb +++ b/indexer/app/lib/indexer_common.rb @@ -3,11 +3,13 @@ require 'json' require 'fileutils' require 'aspace_i18n' +require 'set' require 'asutils' require 'jsonmodel' require 'jsonmodel_client' require 'config/config-distribution' +require 'record_inheritance' require_relative 'index_batch' @@ -16,12 +18,23 @@ class CommonIndexer include JSONModel - @@record_types = [ :top_container,:container_profile, :location_profile, - :archival_object, :resource, - :digital_object, :digital_object_component, - :subject, :location, :classification, :classification_term, - :event, :accession, - :agent_person, :agent_software, :agent_family, :agent_corporate_entity] + @@record_types = [:resource, + :digital_object, + :accession, + :agent_person, + :agent_software, + :agent_family, + :agent_corporate_entity, + :subject, + :location, + :event, + :top_container, + :classification, + :container_profile, + :location_profile, + :archival_object, + :digital_object_component, + :classification_term] @@global_types = [:agent_person, :agent_software, :agent_family, :agent_corporate_entity, :location, :subject] @@ -29,7 +42,11 @@ class CommonIndexer @@records_with_children = [] @@init_hooks = [] - @@resolved_attributes = ['location_profile', 'container_profile', 'container_locations', 'subjects', 'linked_agents', 'linked_records', 'classifications', 'digital_object'] + @@resolved_attributes = ['location_profile', 'container_profile', + 'container_locations', 'subjects', + 'linked_agents', 'linked_records', + 'classifications', 'digital_object', + 'agent_representation', 'repository'] @@paused_until = Time.now @@ -41,6 +58,14 @@ def self.add_attribute_to_resolve(attr) @@resolved_attributes.push(attr) unless @@resolved_attributes.include?(attr) end + def resolved_attributes + @@resolved_attributes + end + + def record_types + @@record_types + end + # This is to pause the indexer. # Duration is given in seconds. def self.pause(duration = 900 ) @@ -77,6 +102,15 @@ def initialize(backend_url) end end + + def self.generate_years_for_date_range(begin_date, end_date) + return [] unless begin_date + b = begin_date[0..3] + e = (end_date || begin_date)[0..3] + (b .. e).to_a + end + + def self.generate_permutations_for_identifier(identifer) return [] if identifer.nil? @@ -113,6 +147,23 @@ def self.extract_string_values(doc) end + def self.build_fullrecord(record) + fullrecord = CommonIndexer.extract_string_values(record) + %w(finding_aid_subtitle finding_aid_author).each do |field| + if record['record'].has_key?(field) + fullrecord << "#{record['record'][field]} " + end + end + + if record['record'].has_key?('names') + fullrecord << record['record']['names'].map {|name| + CommonIndexer.extract_string_values(name) + }.join(" ") + end + fullrecord + end + + def add_agents(doc, record) if record['record']['linked_agents'] # index all linked agents first @@ -162,6 +213,20 @@ def add_notes(doc, record) end + def add_years(doc, record) + if record['record']['dates'] + doc['years'] = [] + record['record']['dates'].each do |date| + doc['years'] += CommonIndexer.generate_years_for_date_range(date['begin'], date['end']) + end + unless doc['years'].empty? + doc['years'] = doc['years'].sort.uniq + doc['year_sort'] = doc['years'].first.rjust(4, '0') + doc['years'].last.rjust(4, '0') + end + end + end + + def add_level(doc, record) if record['record'].has_key? 'level' doc['level'] = (record['record']['level'] === 'otherlevel') ? record['record']['other_level'] : record['record']['level'] @@ -184,6 +249,8 @@ def add_summary(doc, record) end end + # TODO: We should fix this to read from the JSON schemas + HARDCODED_ENUM_FIELDS = ["relator", "type", "role", "source", "rules", "acquisition_type", "resource_type", "processing_priority", "processing_status", "era", "calendar", "digital_object_type", "level", "processing_total_extent_type", "container_extent_type", "extent_type", "event_type", "type_1", "type_2", "type_3", "salutation", "outcome", "finding_aid_description_rules", "finding_aid_status", "instance_type", "use_statement", "checksum_method", "language", "date_type", "label", "certainty", "scope", "portion", "xlink_actuate_attribute", "xlink_show_attribute", "file_format_name", "temporary", "name_order", "country", "jurisdiction", "rights_type", "ip_status", "term_type", "enum_1", "enum_2", "enum_3", "enum_4", "relator_type", "job_type"] def configure_doc_rules @@ -202,26 +269,37 @@ def configure_doc_rules } - add_document_prepare_hook { |doc,record| - ["relator", "type", "role", "source", "rules", "acquisition_type", "resource_type", "processing_priority", "processing_status", "era", "calendar", "digital_object_type", "level", "processing_total_extent_type", "container_extent_type", "extent_type", "event_type", "type_1", "type_2", "type_3", "salutation", "outcome", "finding_aid_description_rules", "finding_aid_status", "instance_type", "use_statement", "checksum_method", "language", "date_type", "label", "certainty", "scope", "portion", "xlink_actuate_attribute", "xlink_show_attribute", "file_format_name", "temporary", "name_order", "country", "jurisdiction", "rights_type", "ip_status", "term_type", "enum_1", "enum_2", "enum_3", "enum_4", "relator_type", "job_type"].each do |field| - Array( ASUtils.search_nested(record["record"], field) ).each { |val| doc["#{field}_enum_s"] ||= []; doc["#{field}_enum_s"] << val } + add_document_prepare_hook {|doc, record| + found_keys = Set.new - end - Array( ASUtils.search_nested(record["record"], "items") ).each do |val| - begin - next unless val.key?("type") - doc["type_enum_s"] ||= []; - doc["type_enum_s"] << val["type"] - rescue - next + ASUtils.search_nested(record["record"], HARDCODED_ENUM_FIELDS, ['_resolved']) do |field, field_value| + key = "#{field}_enum_s" + + doc[key] ||= Set.new + doc[key] << field_value + + found_keys << key + end + + ASUtils.search_nested(record["record"], ['items'], ['_resolved']) do |field, field_value| + if field_value.is_a?(Hash) && field_value.key?('type') + doc['type_enum_s'] ||= Set.new + doc['type_enum_s'] << field_value.fetch('type') + found_keys << 'type_enum_s' + end + end + + # Turn our sets back into regular arrays so they serialize out to JSON correctly + found_keys.each do |key| + doc[key] = doc[key].to_a end - end } add_document_prepare_hook {|doc, record| if doc['primary_type'] == 'archival_object' doc['resource'] = record['record']['resource']['ref'] if record['record']['resource'] doc['title'] = record['record']['display_string'] + doc['component_id'] = record['record']['component_id'] end } @@ -230,6 +308,7 @@ def configure_doc_rules add_agents(doc, record) add_audit_info(doc, record) add_notes(doc, record) + add_years(doc, record) add_level(doc, record) add_summary(doc, record) } @@ -246,6 +325,9 @@ def configure_doc_rules doc['restrictions_apply'] = record['record']['restrictions_apply'] doc['access_restrictions'] = record['record']['access_restrictions'] doc['use_restrictions'] = record['record']['use_restrictions'] + doc['related_resource_uris'] = record['record']['related_resources']. + collect{|resource| resource["ref"]}. + compact.uniq end } @@ -257,11 +339,17 @@ def configure_doc_rules end } + add_document_prepare_hook {|doc, record| + if record['record'].has_key?('used_within_repositories') + doc['used_within_repository'] = record['record']['used_within_repositories'] + end + } + add_document_prepare_hook {|doc, record| if doc['primary_type'] == 'repository' doc['repository'] = doc["id"] doc['title'] = record['record']['repo_code'] - doc['publish'] = true + doc['repo_sort'] = record['record']['display_string'] end } @@ -295,6 +383,9 @@ def configure_doc_rules doc['restrictions'] = record['record']['restrictions'] doc['ead_id'] = record['record']['ead_id'] doc['finding_aid_status'] = record['record']['finding_aid_status'] + doc['related_accession_uris'] = record['record']['related_accessions']. + collect{|accession| accession["ref"]}. + compact.uniq end if doc['primary_type'] == 'digital_object' @@ -303,6 +394,10 @@ def configure_doc_rules doc['digital_object_id'] = record['record']['digital_object_id'] doc['level'] = record['record']['level'] doc['restrictions'] = record['record']['restrictions'] + + doc['linked_instance_uris'] = record['record']['linked_instances']. + collect{|instance| instance["ref"]}. + compact.uniq end } @@ -337,6 +432,8 @@ def configure_doc_rules doc['publish'] = record['record']['publish'] && record['record']['is_linked_to_published_record'] doc['linked_agent_roles'] = record['record']['linked_agent_roles'] + doc['related_agent_uris'] = ASUtils.wrap(record['record']['related_agents']).collect{|ra| ra['ref']} + # Assign the additional type of 'agent' doc['types'] << 'agent' end @@ -348,10 +445,10 @@ def configure_doc_rules end } - add_document_prepare_hook {|doc, record| if ['classification', 'classification_term'].include?(doc['primary_type']) doc['classification_path'] = ASUtils.to_json(record['record']['path_from_root']) + doc['agent_uris'] = ASUtils.wrap(record['record']['creator']).collect{|agent| agent['ref']} end } @@ -377,6 +474,10 @@ def configure_doc_rules collect{|instance| instance["container"]}.compact. collect{|container| container["container_locations"]}.flatten. collect{|container_location| container_location["ref"]}.uniq + doc['digital_object_uris'] = record['record']['instances']. + collect{|instance| instance["digital_object"]}.compact. + collect{|digital_object_instance| digital_object_instance["ref"]}. + flatten.uniq end } @@ -448,6 +549,14 @@ def configure_doc_rules if instance['sub_container'] && instance['sub_container']['top_container'] doc['top_container_uri_u_sstr'] ||= [] doc['top_container_uri_u_sstr'] << instance['sub_container']['top_container']['ref'] + if instance['sub_container']['type_2'] + doc['child_container_u_sstr'] ||= [] + doc['child_container_u_sstr'] << "#{instance['sub_container']['type_2']} #{instance['sub_container']['indicator_2']}" + end + if instance['sub_container']['type_3'] + doc['grand_child_container_u_sstr'] ||= [] + doc['grand_child_container_u_sstr'] << "#{instance['sub_container']['type_3']} #{instance['sub_container']['indicator_2']}" + end end } end @@ -471,18 +580,7 @@ def configure_doc_rules add_document_prepare_hook { |doc, record| - doc['fullrecord'] = CommonIndexer.extract_string_values(record) - %w(finding_aid_subtitle finding_aid_author).each do |field| - if record['record'].has_key?(field) - doc['fullrecord'] << "#{record['record'][field]} " - end - end - - if record['record'].has_key?('names') - doc['fullrecord'] << record['record']['names'].map {|name| - CommonIndexer.extract_string_values(name) - }.join(" ") - end + doc['fullrecord'] = CommonIndexer.build_fullrecord(record) } @@ -506,7 +604,6 @@ def configure_doc_rules end } - record_has_children('collection_management') add_extra_documents_hook {|record| docs = [] @@ -529,7 +626,7 @@ def configure_doc_rules 'processing_hours_total' => cm['processing_hours_total'], 'processing_funding_source' => cm['processing_funding_source'], 'processors' => cm['processors'], - 'suppressed' => record['record']['suppressed'].to_s, + 'suppressed' => record['record']['suppressed'], 'repository' => get_record_scope(record['uri']), 'created_by' => cm['created_by'], 'last_modified_by' => cm['last_modified_by'], @@ -622,6 +719,15 @@ def get_record_scope(uri) end + def is_repository_unpublished?(uri, values) + repo_id = get_record_scope(uri) + + return false if (repo_id == "global") + + values['repository']['_resolved']['publish'] == false + end + + def delete_records(records) return if records.empty? @@ -679,63 +785,130 @@ def clean_whitespace(doc) end - def index_records(records) - batch = IndexBatch.new - - records = dedupe_by_uri(records) + def clean_for_sort(value) + out = value.gsub(/<[^>]+>/, '') + out.gsub!(/-/, ' ') + out.gsub!(/[^\w\s]/, '') + out.strip + end - records.each do |record| - values = record['record'] - uri = record['uri'] - reference = JSONModel.parse_reference(uri) - record_type = reference && reference[:type] - if !record_type || (record_type != 'repository' && !@@record_types.include?(record_type.intern)) - next - end + class IndexerTiming + def initialize + @metrics = {} + end - doc = {} + def add(metric, val) + @metrics[metric] ||= 0 + @metrics[metric] += val.to_i + end - doc['id'] = uri + def to_s + subtotal = @metrics.values.inject(0) {|a, b| a + b} - if ( !values["finding_aid_filing_title"].nil? && values["finding_aid_filing_title"].length > 0 ) - doc['title'] = values["finding_aid_filing_title"] + if @total + # If we have a total, report any difference between the total and the + # numbers we have. + add(:other, @total - subtotal) else - doc['title'] = values['title'] + # Otherwise, just tally up our numbers to determine the total. + @total = subtotal end - doc['primary_type'] = record_type - doc['types'] = [record_type] - doc['json'] = ASUtils.to_json(values) - doc['suppressed'] = values.has_key?('suppressed') ? values['suppressed'].to_s : 'false' - if doc['suppressed'] == 'true' - doc['publish'] = 'false' - elsif values['has_unpublished_ancestor'] - doc['publish'] = 'false' - else - doc['publish'] = values.has_key?('publish') ? values['publish'].to_s : 'false' - end - doc['system_generated'] = values.has_key?('system_generated') ? values['system_generated'].to_s : 'false' - doc['repository'] = get_record_scope(uri) + "#{@total.to_i} ms (#{@metrics.map {|k, v| "#{k}: #{v}"}.join('; ')})" + end - @document_prepare_hooks.each do |hook| - hook.call(doc, record) + def total=(ms) + @total = ms + end + + def time_block(metric) + start_time = Time.now + begin + yield + ensure + add(metric, ((Time.now.to_f * 1000) - (start_time.to_f * 1000))) end + end + end + + def index_records(records, timing = IndexerTiming.new) + batch = IndexBatch.new + + records = dedupe_by_uri(records) + + timing.time_block(:conversion_ms) do + records.each do |record| + values = record['record'] + uri = record['uri'] + + reference = JSONModel.parse_reference(uri) + record_type = reference && reference[:type] + + if !record_type || skip_index_record?(record) || (record_type != 'repository' && !record_types.include?(record_type.intern)) + next + end + + doc = {} + + doc['id'] = uri + doc['uri'] = uri + + if ( !values["finding_aid_filing_title"].nil? && values["finding_aid_filing_title"].length > 0 ) + doc['title'] = values["finding_aid_filing_title"] + else + doc['title'] = values['title'] + end + + doc['primary_type'] = record_type + doc['types'] = [record_type] + doc['json'] = ASUtils.to_json(values) + doc['suppressed'] = values.has_key?('suppressed') && values['suppressed'] + if doc['suppressed'] + doc['publish'] = false + elsif is_repository_unpublished?(uri, values) + doc['publish'] = false + elsif values['is_repository_unpublished'] + doc['publish'] = false + else + doc['publish'] = values.has_key?('publish') && values['publish'] + end + doc['system_generated'] = values.has_key?('system_generated') ? values['system_generated'].to_s : 'false' + doc['repository'] = get_record_scope(uri) + + @document_prepare_hooks.each do |hook| + hook.call(doc, record) + end + + doc['title_sort'] = clean_for_sort(doc['title']) - batch << clean_whitespace(doc) + # do this last of all so we know for certain the doc is published + apply_pui_fields(doc, record) - # Allow a single record to spawn multiple Solr documents if desired - @extra_documents_hooks.each do |hook| - batch.concat(hook.call(record)) + next if skip_index_doc?(doc) + + batch << clean_whitespace(doc) + + # Allow a single record to spawn multiple Solr documents if desired + @extra_documents_hooks.each do |hook| + batch.concat(hook.call(record)) + end end end + index_batch(batch, timing) + + timing + end - # Allow hooks to operate on the entire batch if desired - @batch_hooks.each_with_index do |hook| - hook.call(batch) - end + def index_batch(batch, timing = IndexerTiming.new) + timing.time_block(:batch_hooks_ms) do + # Allow hooks to operate on the entire batch if desired + @batch_hooks.each_with_index do |hook| + hook.call(batch) + end + end if !batch.empty? # For any record we're updating, delete any child records first (where applicable) @@ -756,21 +929,24 @@ def index_records(records) req = Net::HTTP::Post.new("#{solr_url.path}/update") req['Content-Type'] = 'application/json' + # Note: We call to_json_stream before asking for the count because this + # writes out the closing array and newline. stream = batch.to_json_stream req['Content-Length'] = batch.byte_count req.body_stream = stream - response = do_http_request(solr_url, req) + timing.time_block(:solr_add_ms) do + response = do_http_request(solr_url, req) - stream.close - batch.destroy + stream.close + batch.destroy - if response.code != '200' - raise "Error when indexing records: #{response.body}" + if response.code != '200' + raise "Error when indexing records: #{response.body}" + end end end - end @@ -794,7 +970,51 @@ def paused? self.singleton_class.class_variable_get(:@@paused_until) > Time.now end + def skip_index_record?(record) + false + end + + def skip_index_doc?(doc) + false + end + def apply_pui_fields(doc, record) + # only add pui types if the record is published + if doc['publish'] + object_record_types = ['accession', 'digital_object', 'digital_object_component'] + + if object_record_types.include?(doc['primary_type']) + doc['types'] << 'pui_record' + end + + if ['agent_person', 'agent_corporate_entity'].include?(doc['primary_type']) + doc['types'] << 'pui_agent' + end + + unless RecordInheritance.has_type?(doc['primary_type']) + # All record types are available to PUI except archival objects, since + # our pui_indexer indexes a specially formatted version of those. + if ['resource'].include?(doc['primary_type']) + doc['types'] << 'pui_collection' + elsif ['classification'].include?(doc['primary_type']) + doc['types'] << 'pui_record_group' + elsif ['agent_person'].include?(doc['primary_type']) + doc['types'] << 'pui_person' + else + doc['types'] << 'pui_' + doc['primary_type'] + end + + doc['types'] << 'pui' + end + end + + # index all top containers for pui + if doc['primary_type'] == 'top_container' + doc['publish'] = true + doc['types'] << 'pui_container' + doc['types'] << 'pui' + end + end end diff --git a/indexer/app/lib/periodic_indexer.rb b/indexer/app/lib/periodic_indexer.rb index 8f6c412c30..6a96f04547 100644 --- a/indexer/app/lib/periodic_indexer.rb +++ b/indexer/app/lib/periodic_indexer.rb @@ -2,30 +2,17 @@ require 'time' require 'thread' require 'java' -require 'singleton' - -java_import 'java.util.concurrent.ThreadPoolExecutor' -java_import 'java.util.concurrent.TimeUnit' -java_import 'java.util.concurrent.LinkedBlockingQueue' -java_import 'java.util.concurrent.FutureTask' -java_import 'java.util.concurrent.Callable' # Eagerly load this constant since we access it from multiple threads. Having # two threads try to load it simultaneously seems to create the possibility for # race conditions. java.util.concurrent.TimeUnit::MILLISECONDS -# a place for trees... -# not totaly sure how i feel about this... -class ProcessedTrees < java.util.concurrent.ConcurrentHashMap - include Singleton -end - -# we store the state of uri's index in the indexer_State directory +# we store the state of uri's index in the indexer_state directory class IndexState - def initialize - @state_dir = File.join(AppConfig[:data_directory], "indexer_state") + def initialize(state_dir = nil) + @state_dir = state_dir || File.join(AppConfig[:data_directory], "indexer_state") end @@ -38,26 +25,9 @@ def path_for(repository_id, record_type) def set_last_mtime(repository_id, record_type, time) path = path_for(repository_id, record_type) - # We use the Java interfaces here to work around the fact that JRuby 1.7.22 - # punted on throwing write error exceptions. RubyIO.java contained this: - # - # public IRubyObject close_write(ThreadContext context) { - # [...] - # } catch (IOException ioe) { - # // hmmmm - # } - # - # Newer versions of JRuby fix that issue, so we could switch back to the - # original implementation once JRuby is upgraded. For reference, here's the - # original: - # - # File.open("#{path}.tmp", "w") do |fh| - # fh.puts(time.to_i) - # end - # - writer = java.io.PrintWriter.new("#{path}.tmp") - writer.println(time.to_i) - writer.close + File.open("#{path}.tmp", "w") do |fh| + fh.puts(time.to_i) + end File.rename("#{path}.tmp", "#{path}.dat") end @@ -79,218 +49,83 @@ def get_last_mtime(repository_id, record_type) end -## this is the task that will be called to run the indexer -class PeriodicIndexerTask - include Callable - - def initialize(params) - @params = params - end - - # how we run the worker - def call - PeriodicIndexerWorker.new(@params).run - end - -end - - -# not really a worker...just some temp we hire to do a task -# we kill this guy after he's done his job ( don't tell him ) -class PeriodicIndexerWorker < CommonIndexer - - # this is ugly - def initialize(params) - super(AppConfig[:backend_url]) - @state = params[:state] || IndexState.new - @record_type = params[:record_type] || "repository" - @id_set = params[:id_set] || [] - @repo_id = params[:repo_id] || "0" - @session = params[:session] - @indexed_count = params[:indexed_count] || 0 - end - - # this was pulled from the original pindexer - def load_tree_docs(tree, result, root_uri, path_to_root = [], index_whole_tree = false) - return unless tree['publish'] - - this_node = tree.reject {|k, v| k == 'children'} - - direct_children = tree['children']. - reject {|child| child['has_unpublished_ancestor'] || !child['publish'] || child['suppressed']}. - map {|child| - grand_children = child['children'].reject{|grand_child| grand_child['has_unpublished_ancestor'] || !grand_child['publish'] || grand_child['suppressed']} - child['has_children'] = !grand_children.empty? - child.reject {|k, v| k == 'children'} - } - - this_node['has_children'] = !direct_children.empty? - - doc = { - 'id' => "tree_view_#{tree['record_uri']}", - 'primary_type' => 'tree_view', - 'types' => ['tree_view'], - 'exclude_by_default' => 'true', - 'node_uri' => tree['record_uri'], - 'repository' => JSONModel.repository_for(tree['record_uri']), - 'root_uri' => root_uri, - 'publish' => true, - 'tree_json' => ASUtils.to_json(:self => this_node, - :path_to_root => path_to_root, - :direct_children => direct_children) - } - - # For the root node, store a copy of the whole tree - if index_whole_tree && path_to_root.empty? - doc['whole_tree_json'] = ASUtils.to_json(tree) - end - - result << doc - doc = nil - - tree['children'].each do |child| - load_tree_docs(child, result, root_uri, path_to_root + [this_node], index_whole_tree) - end - end - - # also pulled from the original pindexer - def delete_trees_for(resource_uris) - return if resource_uris.empty? +class PeriodicIndexer < CommonIndexer - resource_uris.each_slice(512) do |resource_uris| - req = Net::HTTP::Post.new("#{solr_url.path}/update") - req['Content-Type'] = 'application/json' + def initialize(backend_url = nil, state = nil, indexer_name = nil) + super(backend_url || AppConfig[:backend_url]) - escaped = resource_uris.map {|s| "\"#{s}\""} - delete_request = {'delete' => {'query' => "primary_type:tree_view AND root_uri:(#{escaped.join(' OR ')})"}} + @indexer_name = indexer_name || 'PeriodicIndexer' + @state = state || IndexState.new - req.body = delete_request.to_json + # A small window to account for the fact that transactions might be committed + # after the periodic indexer has checked for updates, but with timestamps from + # prior to the check. + @window_seconds = 30 - response = do_http_request(solr_url, req) + @time_to_sleep = AppConfig[:solr_indexing_frequency_seconds].to_i + @thread_count = AppConfig[:indexer_thread_count].to_i + @records_per_thread = AppConfig[:indexer_records_per_thread].to_i - if response.code != '200' - raise "Error when deleting record trees: #{response.body}" - end - end + @timing = IndexerTiming.new end + def start_worker_thread(queue, record_type) + repo_id = JSONModel.repository + session = JSONModel::HTTP.current_backend_session - # this is where we configure how the solr doc is generated - def configure_doc_rules - super - - add_batch_hook {|batch| - records = batch.map {|rec| - if ['resource', 'digital_object', 'classification'].include?(rec['primary_type']) - rec['id'] - elsif rec['primary_type'] == 'archival_object' - rec['resource'] - elsif rec['primary_type'] == 'digital_object_component' - rec['digital_object'] - elsif rec['primary_type'] == 'classification_term' - rec['classification'] - else - nil + Thread.new do + begin + # Inherit the repo_id and user session from the parent thread + JSONModel.set_repository(repo_id) + JSONModel::HTTP.current_backend_session = session + + did_something = false + + while true + id_subset = queue.poll(10000, java.util.concurrent.TimeUnit::MILLISECONDS) + + # If the parent thread has finished, it should have pushed a :finished + # token. But if we time out after a reasonable amount of time, assume + # it isn't coming back. + break if (id_subset == :finished || id_subset.nil?) + + records = @timing.time_block(:record_fetch_ms) do + fetch_records(record_type, id_subset, resolved_attributes) + end + + if !records.empty? + did_something = true + index_records(records.map {|record| + { + 'record' => record.to_hash(:trusted), + 'uri' => record.uri + } + }) + end end - }.compact.uniq - - # Don't reprocess trees we've already covered during previous batches - records -= ProcessedTrees.instance.keySet - ## Each record needs its tree indexed - - # Delete any existing versions - delete_trees_for(records) - - records.each do |record_uri| - # To avoid all of the indexing threads hitting the same tree at the same - # moment, use @processed_trees to ensure that only one of them handles - # it. - next if ProcessedTrees.instance.putIfAbsent(record_uri, true) - - record_data = JSONModel.parse_reference(record_uri) - - tree = JSONModel("#{record_data[:type]}_tree".intern).find(nil, "#{record_data[:type]}_id".intern => record_data[:id]) - - load_tree_docs(tree.to_hash(:trusted), batch, record_uri, [], - ['classification'].include?(record_data[:type])) - ProcessedTrees.instance.put(record_uri, true) - end - } - end - - def run - begin - t_0 = Time.now - - if @session - JSONModel::HTTP.current_backend_session = @session - else - login - end - - JSONModel.set_repository(@repo_id) - - # Inherit the repo_id and user session from the parent thread - records = JSONModel(@record_type).all(:id_set => @id_set.join(","), - 'resolve[]' => @@resolved_attributes) - - - if records.empty? - return false - else records.empty? - index_records(records.map {|record| - { - 'record' => record.to_hash(:trusted), - 'uri' => record.uri - } - }) + did_something + rescue + $stderr.puts("Failure in #{@indexer_name} worker thread: #{$!}") + raise $! end - - t_1 = Time.now - time_ms = (t_1-t_0) * 1000.0 - return [ records.length, time_ms, @indexed_count ] - - rescue - $stderr.puts("Failure in periodic indexer worker thread: #{$!}") - raise $! end end -end - -# this is the master who runs the tasks. also handles deletes and 'easy' tasks -# ( like indexing repositories ) . -class PeriodicIndexer < CommonIndexer - - # A small window to account for the fact that transactions might be committed - # after the periodic indexer has checked for updates, but with timestamps from - # prior to the check. - WINDOW_SECONDS = 30 - - THREAD_COUNT = AppConfig[:indexer_thread_count].to_i - RECORDS_PER_THREAD = AppConfig[:indexer_records_per_thread].to_i - - def initialize(state = nil) - super(AppConfig[:backend_url]) - @state = state || IndexState.new - end - - def run_index_round - $stderr.puts "#{Time.now}: Running index round" + log("Running index round") login # Index any repositories that were changed start = Time.now - repositories = JSONModel(:repository).all + repositories = JSONModel(:repository).all('resolve[]' => resolved_attributes) - modified_since = [@state.get_last_mtime('repositories', 'repositories') - WINDOW_SECONDS, 0].max + modified_since = [@state.get_last_mtime('repositories', 'repositories') - @window_seconds, 0].max updated_repositories = repositories.reject {|repository| Time.parse(repository['system_mtime']).to_i < modified_since}. map {|repository| { - 'record' => repository.to_hash(:trusted), + 'record' => repository.to_hash(:raw), 'uri' => repository.uri } } @@ -304,22 +139,18 @@ def run_index_round @state.set_last_mtime('repositories', 'repositories', start) - # Set the list of tree URIs back to empty to start over again - ProcessedTrees.instance.clear - # And any records in any repositories repositories.each_with_index do |repository, i| JSONModel.set_repository(repository.id) - did_something = false + checkpoints = [] + did_something = false - # we roll through all our record types - @@record_types.each do |type| - + record_types.each do |type| next if @@global_types.include?(type) && i > 0 start = Time.now - modified_since = [@state.get_last_mtime(repository.id, type) - WINDOW_SECONDS, 0].max + modified_since = [@state.get_last_mtime(repository.id, type) - @window_seconds, 0].max # we get all the ids of this record type out of the repo id_set = JSONModel::HTTP.get_json(JSONModel(type).uri_for, :all_ids => true, :modified_since => modified_since) @@ -327,77 +158,59 @@ def run_index_round next if id_set.empty? indexed_count = 0 - - # this will manage our treaded tasks - executor = ThreadPoolExecutor.new(THREAD_COUNT, THREAD_COUNT, 5000, java.util.concurrent.TimeUnit::MILLISECONDS, LinkedBlockingQueue.new) - tasks = [] - + + work_queue = java.util.concurrent.LinkedBlockingQueue.new(@thread_count) + + workers = (0...@thread_count).map {|thread_idx| + start_worker_thread(work_queue, type) + } + begin - # lets take it one chunk ata time - id_set.each_slice( RECORDS_PER_THREAD * THREAD_COUNT ) do |id_subset| - - - # now we load a task with the number of tasks - id_subset.each_slice(RECORDS_PER_THREAD) do |set| - indexed_count += set.length - task_order = { :repo_id => repository.id, - :session => JSONModel::HTTP.current_backend_session, - :record_type => type, - :id_set => set, - :state => @state, - :indexed_count => indexed_count - } - task = FutureTask.new( PeriodicIndexerTask.new( task_order ) ) - - # execute the task.. - executor.execute(task) - tasks << task - end - - # we're blocking here until all the tasks are completed - tasks.map! do |t| - count, time, counter = t.get - next unless count # if the worker returned false, we move on - $stderr.puts "~~~ Indexed #{counter} of #{id_set.length} #{type} records in repository #{repository.id} ( added #{count.to_s} records in #{time.to_s}ms ) ~~~" - true + # Feed our worker threads subsets of IDs to process + id_set.each_slice(@records_per_thread) do |id_subset| + # This will block if all threads are currently busy indexing. + while !work_queue.offer(id_subset, 5000, java.util.concurrent.TimeUnit::MILLISECONDS) + # If any of the workers have caught an exception, rethrow it immediately + workers.each do |thread| + thread.value if thread.status.nil? + end end - - # let's check if we did something, unless of course we alread know - # we did something - did_something ||= tasks.any? {|t| t } unless did_something - tasks.clear # clears the tasks.. - - end # done iterating over ids - ensure # Let us be sure that... - # wnce we're done, we instruct the workers to finish up. - executor.shutdown - # we also tell solr to commit - send_commit if did_something - # and lets make sure we clear this out too - ProcessedTrees.instance.clear + + indexed_count += id_subset.length + log("~~~ Indexed #{indexed_count} of #{id_set.length} #{type} records in repository #{repository.repo_code}") + end + + ensure + # Once we're done, instruct the workers to finish up. + @thread_count.times { work_queue.offer(:finished, 5000, java.util.concurrent.TimeUnit::MILLISECONDS) } end + # If any worker reports that they indexed some records, we'll send a + # commit. + results = workers.map {|thread| thread.join; thread.value} + did_something ||= results.any? {|status| status} - # lets update the state... - # moved this to update per each type since before it would only update - # after completely finishing an entire repo ( so if you intterupted it, - # you'd have to start all over again for each repo ) - @state.set_last_mtime(repository.id, type, start) + checkpoints << [repository, type, start] + + $stderr.puts("Indexed #{id_set.length} records in #{Time.now.to_i - start.to_i} seconds") + end - $stderr.puts "~" * 100 - $stderr.puts("~~~ Indexed #{id_set.length} #{type} records in #{Time.now.to_i - start.to_i} seconds ~~~") - $stderr.puts "~" * 100 - end # done iterating over types + index_round_complete(repository) - # courtesy flush for the repo send_commit if did_something - - end # done iterating over repositories + checkpoints.each do |repository, type, start| + @state.set_last_mtime(repository.id, type, start) + end + end - # now lets delete handle_deletes - + + log("Index round complete") + end + + def index_round_complete(repository) + # Give subclasses a place to hang custom behavior. end def handle_deletes @@ -407,7 +220,7 @@ def handle_deletes page = 1 while true - deletes = JSONModel::HTTP.get_json("/delete-feed", :modified_since => [last_mtime - WINDOW_SECONDS, 0].max, :page => page, :page_size => RECORDS_PER_THREAD) + deletes = JSONModel::HTTP.get_json("/delete-feed", :modified_since => [last_mtime - @window_seconds, 0].max, :page => page, :page_size => @records_per_thread) if !deletes['results'].empty? did_something = true @@ -426,27 +239,31 @@ def handle_deletes @state.set_last_mtime('_deletes', 'deletes', start) end - - - def run while true begin run_index_round unless paused? rescue reset_session - puts "#{$!.backtrace.join("\n")}" - - puts "#{$!.inspect}" + log($!.backtrace.join("\n")) + log($!.inspect) end - sleep AppConfig[:solr_indexing_frequency_seconds].to_i + sleep @time_to_sleep end end + def log(line) + $stderr.puts("#{@indexer_name} [#{Time.now}] #{line}") + $stderr.flush + end + + def self.get_indexer(state = nil, name = "Staff Indexer") + indexer = self.new(AppConfig[:backend_url], state, name) + end - def self.get_indexer(state = nil) - indexer = self.new(state) + def fetch_records(type, ids, resolve) + JSONModel(type).all(:id_set => ids.join(","), 'resolve[]' => resolve) end end diff --git a/indexer/app/lib/pui_indexer.rb b/indexer/app/lib/pui_indexer.rb new file mode 100644 index 0000000000..b00510b3d9 --- /dev/null +++ b/indexer/app/lib/pui_indexer.rb @@ -0,0 +1,280 @@ +require 'record_inheritance' + +require_relative 'periodic_indexer' + +require 'set' + +class PUIIndexer < PeriodicIndexer + + PUI_RESOLVES = [ + 'ancestors', + 'ancestors::linked_agents', + 'ancestors::subjects', + 'ancestors::instances::sub_container::top_container' + ] + + def initialize(backend = nil, state = nil, name) + index_state = state || IndexState.new(File.join(AppConfig[:data_directory], "indexer_pui_state")) + + super(backend, index_state, name) + + # Set up our JSON schemas now that we know the JSONModels have been loaded + RecordInheritance.prepare_schemas + + @time_to_sleep = AppConfig[:pui_indexing_frequency_seconds].to_i + @thread_count = AppConfig[:pui_indexer_thread_count].to_i + @records_per_thread = AppConfig[:pui_indexer_records_per_thread].to_i + end + + def fetch_records(type, ids, resolve) + records = JSONModel(type).all(:id_set => ids.join(","), 'resolve[]' => resolve) + if RecordInheritance.has_type?(type) + RecordInheritance.merge(records, :direct_only => true) + else + records + end + end + + def self.get_indexer(state = nil, name = "PUI Indexer") + indexer = self.new(state, name) + end + + def resolved_attributes + super + PUI_RESOLVES + end + + def record_types + # We only want to index the record types we're going to make separate + # PUI-specific versions of... + (super.select {|type| RecordInheritance.has_type?(type)} + [:archival_object]).uniq + end + + def configure_doc_rules + super + + record_has_children('resource') + record_has_children('archival_object') + record_has_children('digital_object') + record_has_children('digital_object_component') + record_has_children('classification') + record_has_children('classification_term') + + + add_document_prepare_hook {|doc, record| + + if RecordInheritance.has_type?(doc['primary_type']) + parent_id = doc['id'] + doc['id'] = "#{parent_id}#pui" + doc['parent_id'] = parent_id + doc['types'] ||= [] + doc['types'] << 'pui' + doc['types'] << "pui_#{doc['primary_type']}" + doc['types'] << 'pui_record' + end + } + + # this runs after the hooks in indexer_common, so we can overwrite with confidence + add_document_prepare_hook {|doc, record| + if RecordInheritance.has_type?(doc['primary_type']) + # special handling for json because we need to include indirectly inherited + # fields too - the json sent to indexer_common only has directly inherited + # fields because only they should be indexed. + # so we remerge without the :direct_only flag, and we remove the ancestors + doc['json'] = ASUtils.to_json(RecordInheritance.merge(record['record'], + :remove_ancestors => true)) + + # special handling for title because it is populated from display_string + # in indexer_common and display_string is not changed in the merge process + doc['title'] = record['record']['title'] if record['record']['title'] + + # special handling for fullrecord because we don't want the ancestors indexed. + # we're now done with the ancestors, so we can just delete them from the record + record['record'].delete('ancestors') + doc['fullrecord'] = CommonIndexer.build_fullrecord(record) + end + } + end + + def add_infscroll_docs(resource_uris, batch) + resource_uris.each do |resource_uri| + json = JSONModel::HTTP.get_json(resource_uri + '/ordered_records') + + batch << { + 'id' => "#{resource_uri}/ordered_records", + 'parent_id' => resource_uri, + 'publish' => "true", + 'primary_type' => "resource_ordered_records", + 'json' => ASUtils.to_json(json) + } + end + end + + + class LargeTreeDocIndexer + + attr_reader :batch + + def initialize(batch) + # We'll track the nodes we find as we need to index their path from root + # in a relatively efficient way + @node_uris = [] + + @batch = batch + end + + def add_largetree_docs(root_record_uris) + root_record_uris.each do |node_uri| + @node_uris.clear + + json = JSONModel::HTTP.get_json(node_uri + '/tree/root', + :published_only => true) + + batch << { + 'id' => "#{node_uri}/tree/root", + 'parent_id' => node_uri, + 'publish' => "true", + 'primary_type' => "tree_root", + 'json' => ASUtils.to_json(json) + } + + add_waypoints(json, node_uri, nil) + + index_paths_to_root(node_uri, @node_uris) + end + end + + def add_waypoints(json, root_record_uri, parent_uri) + json.fetch('waypoints').times do |waypoint_number| + json = JSONModel::HTTP.get_json(root_record_uri + '/tree/waypoint', + :offset => waypoint_number, + :parent_node => parent_uri, + :published_only => true) + + + batch << { + 'id' => "#{root_record_uri}/tree/waypoint_#{parent_uri}_#{waypoint_number}", + 'parent_id' => (parent_uri || root_record_uri), + 'publish' => "true", + 'primary_type' => "tree_waypoint", + 'json' => ASUtils.to_json(json) + } + + json.each do |waypoint_record| + add_nodes(root_record_uri, waypoint_record) + end + end + end + + def add_nodes(root_record_uri, waypoint_record) + record_uri = waypoint_record.fetch('uri') + + @node_uris << record_uri + + # Index the node itself if it has children + if waypoint_record.fetch('child_count') > 0 + json = JSONModel::HTTP.get_json(root_record_uri + '/tree/node', + :node_uri => record_uri, + :published_only => true) + + # We might bomb out if a record was deleted out from under us. + return if json.nil? + + batch << { + 'id' => "#{root_record_uri}/tree/node_#{json.fetch('uri')}", + 'parent_id' => json.fetch('uri'), + 'publish' => "true", + 'primary_type' => "tree_node", + 'json' => ASUtils.to_json(json) + } + + # Finally, walk the node's waypoints and index those too. + add_waypoints(json, root_record_uri, json.fetch('uri')) + end + end + + def index_paths_to_root(root_uri, node_uris) + node_uris.each_slice(128) do |node_uris| + + node_id_to_uri = Hash[node_uris.map {|uri| [JSONModel.parse_reference(uri).fetch(:id), uri]}] + node_paths = JSONModel::HTTP.get_json(root_uri + '/tree/node_from_root', + 'node_ids[]' => node_id_to_uri.keys, + :published_only => true) + + node_paths.each do |node_id, path| + batch << { + 'id' => "#{root_uri}/tree/node_from_root_#{node_id}", + 'parent_id' => node_id_to_uri.fetch(Integer(node_id)), + 'publish' => "true", + 'primary_type' => "tree_node_from_root", + 'json' => ASUtils.to_json({node_id => path}) + } + end + end + end + + end + + + def skip_index_record?(record) + !record['record']['publish'] + end + + + def skip_index_doc?(doc) + !doc['publish'] + end + + def index_round_complete(repository) + # Index any trees in `repository` + tree_types = [[:resource, :archival_object], + [:digital_object, :digital_object_component], + [:classification, :classification_term]] + + start = Time.now + checkpoints = [] + + tree_uris = [] + + tree_types.each do |pair| + root_type = pair.first + node_type = pair.last + + checkpoints << [repository, root_type, start] + checkpoints << [repository, node_type, start] + + last_root_node_mtime = [@state.get_last_mtime(repository.id, root_type) - @window_seconds, 0].max + last_node_mtime = [@state.get_last_mtime(repository.id, node_type) - @window_seconds, 0].max + + root_node_ids = Set.new(JSONModel::HTTP.get_json(JSONModel(root_type).uri_for, :all_ids => true, :modified_since => last_root_node_mtime)) + node_ids = JSONModel::HTTP.get_json(JSONModel(node_type).uri_for, :all_ids => true, :modified_since => last_node_mtime) + + node_ids.each_slice(@records_per_thread) do |ids| + node_records = JSONModel(node_type).all(:id_set => ids.join(","), 'resolve[]' => []) + + node_records.each do |record| + root_node_ids << JSONModel.parse_reference(record[root_type.to_s]['ref']).fetch(:id) + end + end + + tree_uris.concat(root_node_ids.map {|id| JSONModel(root_type).uri_for(id) }) + end + + batch = IndexBatch.new + + add_infscroll_docs(tree_uris.select {|uri| JSONModel.parse_reference(uri).fetch(:type) == 'resource'}, + batch) + + LargeTreeDocIndexer.new(batch).add_largetree_docs(tree_uris) + + log "Indexed #{batch.length} additional PUI records in repository #{repository.repo_code}" + + index_batch(batch) + send_commit + + checkpoints.each do |repository, type, start| + @state.set_last_mtime(repository.id, type, start) + end + + end + +end diff --git a/indexer/app/lib/realtime_indexer.rb b/indexer/app/lib/realtime_indexer.rb index d16ae17d5f..c156646b8e 100644 --- a/indexer/app/lib/realtime_indexer.rb +++ b/indexer/app/lib/realtime_indexer.rb @@ -12,7 +12,7 @@ def initialize(backend_url, should_continue) def get_updates(last_sequence = 0) - resolve_params = @@resolved_attributes.map {|a| "resolve[]=#{a}"}.join("&") + resolve_params = resolved_attributes.map {|a| "resolve[]=#{a}"}.join("&") response = do_http_request(URI.parse(@backend_url), Net::HTTP::Get.new("/update-feed?last_sequence=#{last_sequence}&#{resolve_params}")) @@ -51,8 +51,8 @@ def run_index_round(last_sequence) # Doesn't matter... rescue reset_session - puts "#{$!.inspect}" - puts $@.join("\n") + $stderr.puts("#{$!.inspect}") + $stderr.puts($@.join("\n")) sleep 5 end diff --git a/indexer/app/main.rb b/indexer/app/main.rb index 0f2076154a..cdbe894999 100644 --- a/indexer/app/main.rb +++ b/indexer/app/main.rb @@ -4,17 +4,37 @@ require_relative 'lib/periodic_indexer' require_relative 'lib/realtime_indexer' +require_relative 'lib/pui_indexer' class ArchivesSpaceIndexer < Sinatra::Base def self.main periodic_indexer = PeriodicIndexer.get_indexer + pui_indexer = PUIIndexer.get_indexer threads = [] - puts "Starting periodic indexer" + $stderr.puts "Starting periodic indexer" threads << Thread.new do - periodic_indexer.run + begin + periodic_indexer.run + rescue + $stderr.puts "Unexpected failure in periodic indexer: #{$!}" + end + end + + if AppConfig[:pui_indexer_enabled] + $stderr.puts "Starting PUI indexer" + threads << Thread.new do + # Stagger them to encourage them to run at different times + sleep AppConfig[:solr_indexing_frequency_seconds] + + begin + pui_indexer.run + rescue + $stderr.puts "Unexpected failure in PUI indexer: #{$!}" + end + end end sleep 5 @@ -34,14 +54,14 @@ def self.main backend_urls.value.each do |url| if !realtime_indexers[url] || !realtime_indexers[url].alive? - puts "Starting realtime indexer for: #{url}" + $stderr.puts "Starting realtime indexer for: #{url}" realtime_indexers[url] = Thread.new do begin indexer = RealtimeIndexer.new(url, proc { backend_urls.value.include?(url) }) indexer.run rescue - puts "Realtime indexing error (#{backend_url}): #{$!}" + $stderr.puts "Realtime indexing error (#{backend_url}): #{$!}" sleep 5 end end @@ -83,7 +103,7 @@ def self.main "Running every #{AppConfig[:solr_indexing_frequency_seconds].to_i} seconds. " end end - + # this pauses the indexer so that bulk update and migrations can happen # without bogging down the server put "/" do diff --git a/launcher/archivesspace.bat b/launcher/archivesspace.bat index 7c079dfafe..503e380795 100755 --- a/launcher/archivesspace.bat +++ b/launcher/archivesspace.bat @@ -32,7 +32,7 @@ goto END :STARTUP echo Writing log file to logs\archivesspace.out -java -Darchivesspace-daemon=yes %JAVA_OPTS% -Xss2m -XX:MaxPermSize=256m -Xmx1024m -Dfile.encoding=UTF-8 -cp "%GEM_HOME%\gems\jruby-rack-1.1.12\lib\*;lib\*;launcher\lib\*!JRUBY!" org.jruby.Main --1.9 "launcher/launcher.rb" > "logs/archivesspace.out" 2>&1 +java -Darchivesspace-daemon=yes %JAVA_OPTS% -Xss2m -XX:MaxPermSize=256m -Xmx1024m -Dfile.encoding=UTF-8 -cp "%GEM_HOME%\gems\jruby-rack-1.1.12\lib\*;lib\*;launcher\lib\*!JRUBY!" org.jruby.Main "launcher/launcher.rb" > "logs/archivesspace.out" 2>&1 :END diff --git a/launcher/archivesspace.sh b/launcher/archivesspace.sh index 874ecbff75..601eabcbcc 100755 --- a/launcher/archivesspace.sh +++ b/launcher/archivesspace.sh @@ -93,7 +93,7 @@ if [ "$ARCHIVESSPACE_LOGS" = "" ]; then ARCHIVESSPACE_LOGS="logs/archivesspace.out" fi -export JAVA_OPTS="-Darchivesspace-daemon=yes $JAVA_OPTS" +export JAVA_OPTS="-Darchivesspace-daemon=yes $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom" # Wow. Not proud of this! export JAVA_OPTS="`echo $JAVA_OPTS | sed 's/\([#&;\`|*?~<>^(){}$\,]\)/\\\\\1/g'`" @@ -115,10 +115,11 @@ for dir in "$ASPACE_LAUNCHER_BASE"/gems/gems/jruby-*; do JRUBY="$JRUBY:$dir/lib/*" done + startup_cmd="java "$JAVA_OPTS" \ $ASPACE_JAVA_XMX $ASPACE_JAVA_XSS $ASPACE_JAVA_MAXPERMSIZE -Dfile.encoding=UTF-8 \ -cp \"lib/*:launcher/lib/*$JRUBY\" \ - org.jruby.Main --disable-gems --1.9 \"launcher/launcher.rb\"" + org.jruby.Main --disable-gems \"launcher/launcher.rb\"" export PIDFILE="$ASPACE_LAUNCHER_BASE/data/.archivesspace.pid" diff --git a/launcher/backup/backup.bat b/launcher/backup/backup.bat index b21d6e0583..492ca3eb2b 100644 --- a/launcher/backup/backup.bat +++ b/launcher/backup/backup.bat @@ -13,4 +13,4 @@ FOR /D %%c IN (..\gems\gems\jruby-*) DO ( ) -java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main --1.9 ../launcher/backup/lib/backup.rb %* +java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main ../launcher/backup/lib/backup.rb %* diff --git a/launcher/backup/backup.sh b/launcher/backup/backup.sh index 675e66c48c..25307216fc 100755 --- a/launcher/backup/backup.sh +++ b/launcher/backup/backup.sh @@ -14,4 +14,4 @@ for dir in ../gems/gems/jruby-*; do done -java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main --1.9 ../launcher/backup/lib/backup.rb ${1+"$@"} +java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main ../launcher/backup/lib/backup.rb ${1+"$@"} diff --git a/launcher/backup/lib/backup.rb b/launcher/backup/lib/backup.rb index 2d3f3b58f3..cbc89b5c42 100644 --- a/launcher/backup/lib/backup.rb +++ b/launcher/backup/lib/backup.rb @@ -85,7 +85,7 @@ def create_demodb_snapshot def backup(output_file, do_mysqldump = false) output_file = File.absolute_path(output_file, ENV['ORIG_PWD']) - if File.exists?(output_file) + if File.exist?(output_file) puts "Output file '#{output_file}' already exists! Aborting" return 1 end @@ -114,7 +114,7 @@ def backup(output_file, do_mysqldump = false) Zip::ZipFile.open(output_file, Zip::ZipFile::CREATE) do |zipfile| add_whole_directory(solr_snapshot, zipfile) if AppConfig[:enable_solr] - add_whole_directory(demo_db_backups, zipfile) if Dir.exists?(demo_db_backups) + add_whole_directory(demo_db_backups, zipfile) if Dir.exist?(demo_db_backups) add_whole_directory(config_dir, zipfile) if config_dir zipfile.add("mysqldump.sql", mysql_dump) if mysql_dump end diff --git a/launcher/ead_export/ead_export.bat b/launcher/ead_export/ead_export.bat index 7faa2758d5..94e9da8bd9 100644 --- a/launcher/ead_export/ead_export.bat +++ b/launcher/ead_export/ead_export.bat @@ -11,4 +11,4 @@ FOR /D %%c IN (..\gems\gems\jruby-*) DO ( set JRUBY=!JRUBY!;%%c\lib\* ) -java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main --1.9 ..\launcher\ead_export\lib\ead_export.rb %1 %2 %3 +java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main ..\launcher\ead_export\lib\ead_export.rb %1 %2 %3 diff --git a/launcher/ead_export/ead_export.sh b/launcher/ead_export/ead_export.sh index 1971b5e7db..3a83fc4b77 100755 --- a/launcher/ead_export/ead_export.sh +++ b/launcher/ead_export/ead_export.sh @@ -13,4 +13,4 @@ for dir in ../gems/gems/jruby-*; do done -java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main --1.9 ../launcher/ead_export/lib/ead_export.rb ${1+"$@"} +java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main ../launcher/ead_export/lib/ead_export.rb ${1+"$@"} diff --git a/launcher/ead_export/lib/ead_export.rb b/launcher/ead_export/lib/ead_export.rb index 0cb13597b3..a4eb326678 100644 --- a/launcher/ead_export/lib/ead_export.rb +++ b/launcher/ead_export/lib/ead_export.rb @@ -41,7 +41,7 @@ def main if ids.any? zip_filename = "#{AppConfig[:data_directory]}/export-repo-#{repository_id}.zip" # for now at least blow away any existing zip file - File.delete zip_filename if File.exists? zip_filename + File.delete zip_filename if File.exist? zip_filename zip = Zip::File.new(zip_filename, Zip::File::CREATE) ids.each do |id| diff --git a/launcher/launcher.rb b/launcher/launcher.rb index c0f605191b..1a00836905 100644 --- a/launcher/launcher.rb +++ b/launcher/launcher.rb @@ -96,7 +96,7 @@ def start_server(port, *webapps) def generate_secret_for(secret) file = File.join(AppConfig[:data_directory], "#{secret}_cookie_secret.dat") - if !File.exists?(file) + if !File.exist?(file) File.write(file, SecureRandom.hex) puts "****" @@ -133,6 +133,10 @@ def main if AppConfig[:enable_solr] java.lang.System.set_property("solr.data.dir", AppConfig[:solr_index_directory]) java.lang.System.set_property("solr.solr.home", AppConfig[:solr_home_directory]) + + # Windows complains if this directory is missing. Just create it if needed + # and move on with our lives. + FileUtils.mkdir_p(File.join(AppConfig[:solr_home_directory], "collection1", "conf")) end [:search_user_secret, :public_user_secret, :staff_user_secret].each do |property| @@ -212,7 +216,7 @@ def stop stop_server(URI(AppConfig[:frontend_url])) if AppConfig[:enable_frontend] stop_server(URI(AppConfig[:public_url])) if AppConfig[:enable_public] pid_file = File.join(AppConfig[:data_directory], ".archivesspace.pid" ) - FileUtils.rm(pid_file) if File.exists?(pid_file) + FileUtils.rm(pid_file) if File.exist?(pid_file) java.lang.System.exit(0) else puts "****" @@ -227,7 +231,7 @@ def stop launcher_rc = File.join(java.lang.System.get_property("ASPACE_LAUNCHER_BASE"), "launcher_rc.rb") -if java.lang.System.get_property("ASPACE_LAUNCHER_BASE") && File.exists?(launcher_rc) +if java.lang.System.get_property("ASPACE_LAUNCHER_BASE") && File.exist?(launcher_rc) load File.absolute_path(launcher_rc) end diff --git a/launcher/password_reset/password-reset.bat b/launcher/password_reset/password-reset.bat index fd488c894e..baae499048 100644 --- a/launcher/password_reset/password-reset.bat +++ b/launcher/password_reset/password-reset.bat @@ -11,4 +11,4 @@ FOR /D %%c IN (..\gems\gems\jruby-*) DO ( set JRUBY=!JRUBY!;%%c\lib\* ) -java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main --1.9 ..\launcher\password_reset\lib\password-reset.rb %1 %2 +java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main ..\launcher\password_reset\lib\password-reset.rb %1 %2 diff --git a/launcher/password_reset/password-reset.sh b/launcher/password_reset/password-reset.sh index 59251deea6..3328367991 100755 --- a/launcher/password_reset/password-reset.sh +++ b/launcher/password_reset/password-reset.sh @@ -13,4 +13,4 @@ for dir in ../gems/gems/jruby-*; do done -java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main --1.9 ../launcher/password_reset/lib/password-reset.rb ${1+"$@"} +java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main ../launcher/password_reset/lib/password-reset.rb ${1+"$@"} diff --git a/launcher/plugin_gems/initialize-plugin.bat b/launcher/plugin_gems/initialize-plugin.bat index 1b6c5f0dea..005638a0a8 100755 --- a/launcher/plugin_gems/initialize-plugin.bat +++ b/launcher/plugin_gems/initialize-plugin.bat @@ -10,5 +10,5 @@ FOR /D %%c IN (..\..\gems\gems\jruby-*) DO ( ) set GEM_HOME=gems -java %JAVA_OPTS% -cp "..\..\lib\*!JRUBY!" org.jruby.Main --1.9 -S gem install bundler -java %JAVA_OPTS% -cp "..\..\lib\*!JRUBY!" org.jruby.Main --1.9 -S ..\..\gems\bin\bundle install --gemfile=Gemfile +java %JAVA_OPTS% -cp "..\..\lib\*!JRUBY!" org.jruby.Main -S gem install bundler +java %JAVA_OPTS% -cp "..\..\lib\*!JRUBY!" org.jruby.Main -S ..\..\gems\bin\bundle install --gemfile=Gemfile diff --git a/launcher/plugin_gems/initialize-plugin.sh b/launcher/plugin_gems/initialize-plugin.sh index 1726498c99..57e77603ff 100755 --- a/launcher/plugin_gems/initialize-plugin.sh +++ b/launcher/plugin_gems/initialize-plugin.sh @@ -30,7 +30,7 @@ done export GEM_HOME=gems -java $JAVA_OPTS -cp "../../lib/*$JRUBY" org.jruby.Main --1.9 -S gem install bundler -v "$BUNDLER_VERSION" -java $JAVA_OPTS -cp "../../lib/*$JRUBY" org.jruby.Main --1.9 ../../gems/bin/bundle install --gemfile=Gemfile +java $JAVA_OPTS -cp "../../lib/*$JRUBY" org.jruby.Main -S gem install bundler -v "$BUNDLER_VERSION" +java $JAVA_OPTS -cp "../../lib/*$JRUBY" org.jruby.Main ../../gems/bin/bundle install --gemfile=Gemfile diff --git a/launcher/scripts/checkindex.bat b/launcher/scripts/checkindex.bat index 4880432314..98e7615f80 100755 --- a/launcher/scripts/checkindex.bat +++ b/launcher/scripts/checkindex.bat @@ -11,4 +11,4 @@ FOR /D %%c IN (..\gems\gems\jruby-*) DO ( set JRUBY=!JRUBY!;%%c\lib\* ) -java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main --1.9 ..\scripts\rb\checkindex.rb %* +java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main ..\scripts\rb\checkindex.rb %* diff --git a/launcher/scripts/checkindex.sh b/launcher/scripts/checkindex.sh index 71484cc707..4054ef1173 100755 --- a/launcher/scripts/checkindex.sh +++ b/launcher/scripts/checkindex.sh @@ -14,4 +14,4 @@ for dir in ../gems/gems/jruby-*; do done -java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main --1.9 ../scripts/rb/checkindex.rb ${1+"$@"} +java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main ../scripts/rb/checkindex.rb ${1+"$@"} diff --git a/launcher/scripts/setup-database.bat b/launcher/scripts/setup-database.bat index 34adaccbf7..bcef53148a 100644 --- a/launcher/scripts/setup-database.bat +++ b/launcher/scripts/setup-database.bat @@ -11,4 +11,4 @@ FOR /D %%c IN (..\gems\gems\jruby-*) DO ( set JRUBY=!JRUBY!;%%c\lib\* ) -java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main --1.9 ..\scripts\rb\migrate_db.rb +java %JAVA_OPTS% -cp "..\lib\*!JRUBY!" org.jruby.Main ..\scripts\rb\migrate_db.rb diff --git a/launcher/scripts/setup-database.sh b/launcher/scripts/setup-database.sh index 95c5b86b91..2888b31ee9 100755 --- a/launcher/scripts/setup-database.sh +++ b/launcher/scripts/setup-database.sh @@ -13,4 +13,4 @@ for dir in ../gems/gems/jruby-*; do done -java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main --1.9 ../scripts/rb/migrate_db.rb +java $JAVA_OPTS -cp "../lib/*$JRUBY" org.jruby.Main ../scripts/rb/migrate_db.rb diff --git a/plugins/PLUGINS_README.md b/plugins/PLUGINS_README.md index f4c18b308f..da0658522f 100644 --- a/plugins/PLUGINS_README.md +++ b/plugins/PLUGINS_README.md @@ -31,6 +31,8 @@ be used to override or extend the behavior of the core application. backend controllers ......... backend endpoints model ............... database mapping models + converters .......... classes for importing data + job_runners ......... classes for defining background jobs plugin_init.rb ...... if present, loaded when the backend first starts frontend assets .............. static assets (such as images, javascript) in the staff interface diff --git a/plugins/aspace-public-formats/public/plugin_init.rb b/plugins/aspace-public-formats/public/plugin_init.rb index 3d16a17eed..a9f7277d9b 100644 --- a/plugins/aspace-public-formats/public/plugin_init.rb +++ b/plugins/aspace-public-formats/public/plugin_init.rb @@ -1,6 +1,9 @@ +if ENV['ASPACE_PUBLIC_NEW'] == 'true' + raise 'The aspace-public-formats plugin is only compatible with the original public user interface' +end + require "net/http" require "uri" -my_routes = [File.join(File.dirname(__FILE__), "routes.rb")] -ArchivesSpacePublic::Application.config.paths['config/routes'].concat(my_routes) +ArchivesSpacePublic::Application.extend_aspace_routes(File.join(File.dirname(__FILE__), "routes.rb")) diff --git a/plugins/cat_in_a_box/frontend/controllers/cat_in_a_box_controller.rb b/plugins/cat_in_a_box/frontend/controllers/cat_in_a_box_controller.rb index 34ed33599c..25a9e36840 100644 --- a/plugins/cat_in_a_box/frontend/controllers/cat_in_a_box_controller.rb +++ b/plugins/cat_in_a_box/frontend/controllers/cat_in_a_box_controller.rb @@ -3,7 +3,7 @@ class CatInABoxController < ApplicationController - skip_before_filter :unauthorised_access + skip_before_action :unauthorised_access def index c = Nokogiri::XML(open('http://thecatapi.com/api/images/get?format=xml&category=boxes')) @@ -11,4 +11,4 @@ def index c.at_xpath('//image').elements.each { |n| @cat[n.name.to_sym] = n.inner_text } end -end \ No newline at end of file +end diff --git a/plugins/generate_accession_identifiers/frontend/controllers/generate_accession_identifiers_controller.rb b/plugins/generate_accession_identifiers/frontend/controllers/generate_accession_identifiers_controller.rb index 016d1d1c48..eb96828c19 100644 --- a/plugins/generate_accession_identifiers/frontend/controllers/generate_accession_identifiers_controller.rb +++ b/plugins/generate_accession_identifiers/frontend/controllers/generate_accession_identifiers_controller.rb @@ -1,6 +1,6 @@ class GenerateAccessionIdentifiersController < ApplicationController - skip_before_filter :unauthorised_access + skip_before_action :unauthorised_access def generate response = JSONModel::HTTP::post_form('/plugins/generate_accession_identifiers/next') diff --git a/plugins/generate_accession_identifiers/frontend/plugin_init.rb b/plugins/generate_accession_identifiers/frontend/plugin_init.rb index 2c5f9c5c80..9d9cc28819 100644 --- a/plugins/generate_accession_identifiers/frontend/plugin_init.rb +++ b/plugins/generate_accession_identifiers/frontend/plugin_init.rb @@ -1,2 +1 @@ -my_routes = [File.join(File.dirname(__FILE__), "routes.rb")] -ArchivesSpace::Application.config.paths['config/routes'].concat(my_routes) +ArchivesSpace::Application.extend_aspace_routes(File.join(File.dirname(__FILE__), "routes.rb")) diff --git a/plugins/hello_world/frontend/controllers/hello_world_controller.rb b/plugins/hello_world/frontend/controllers/hello_world_controller.rb index 3d5560b600..ba1d47f8d9 100644 --- a/plugins/hello_world/frontend/controllers/hello_world_controller.rb +++ b/plugins/hello_world/frontend/controllers/hello_world_controller.rb @@ -1,6 +1,6 @@ class HelloWorldController < ApplicationController - skip_before_filter :unauthorised_access + skip_before_action :unauthorised_access def index @whosaidhello = JSONModel::HTTP::get_json('/whosaidhello') diff --git a/plugins/jobs_example/README.md b/plugins/jobs_example/README.md new file mode 100644 index 0000000000..b83027eb08 --- /dev/null +++ b/plugins/jobs_example/README.md @@ -0,0 +1,22 @@ +ArchivesSpace jobs_example plugin +================================= + +This is an example plugin for adding a background job type. + +## Getting Started + +Enable the plugin by editing the file in `config/config.rb`: + + AppConfig[:plugins] = ['some_plugin', 'jobs_example'] + + +## What does it do? + +It adds a new kind of job called `slow_nothing_job`. It doesn't +do anything except sleep for 10 seconds a parameterizable number +of times, and log the fact. It can also be canceled, and can +report its success. + +It is intended to demonstrate a minimal implementation for +adding a job type from a plugin. + diff --git a/plugins/jobs_example/backend/job_runners/slow_nothing_runner.rb b/plugins/jobs_example/backend/job_runners/slow_nothing_runner.rb new file mode 100644 index 0000000000..6359a011b4 --- /dev/null +++ b/plugins/jobs_example/backend/job_runners/slow_nothing_runner.rb @@ -0,0 +1,33 @@ +require 'java' +import java.lang.management.ManagementFactory + +class SlowNothingRunner < JobRunner + + register_for_job_type('slow_nothing_job', :run_concurrently => true) + + def run + @json.job['times'].to_i.times do |i| + if self.canceled? + log("Oh gee, I've been canceled") + break + end + + memory = ManagementFactory.memory_mx_bean + log("#{i+1} of #{@json.job['times']}") + log(Time.now) + log("Heap: #{memory.heap_memory_usage}") + log("Non-heap: #{memory.non_heap_memory_usage}") + log("Finalize count: #{memory.object_pending_finalization_count}") + log("===") + sleep 10 + end + log("Phew, I'm done doing nothing!") + self.success! + end + + def log(s) + Log.debug(s) + @job.write_output(s) + end + +end diff --git a/plugins/jobs_example/frontend/locales/en.yml b/plugins/jobs_example/frontend/locales/en.yml new file mode 100644 index 0000000000..20d5bd48b0 --- /dev/null +++ b/plugins/jobs_example/frontend/locales/en.yml @@ -0,0 +1,8 @@ +en: + slow_nothing_job: + _singular: Slow nothing job + _plural: Slow nothing jobs + times: How many times? + job: + types: + slow_nothing_job: Slow Nothing Job diff --git a/plugins/jobs_example/frontend/views/slow_nothing_job/_form.html.erb b/plugins/jobs_example/frontend/views/slow_nothing_job/_form.html.erb new file mode 100644 index 0000000000..ebd890b8c3 --- /dev/null +++ b/plugins/jobs_example/frontend/views/slow_nothing_job/_form.html.erb @@ -0,0 +1 @@ +<%= form.label_and_textfield "times" %> diff --git a/plugins/jobs_example/frontend/views/slow_nothing_job/_show.html.erb b/plugins/jobs_example/frontend/views/slow_nothing_job/_show.html.erb new file mode 100644 index 0000000000..2ffad6d260 --- /dev/null +++ b/plugins/jobs_example/frontend/views/slow_nothing_job/_show.html.erb @@ -0,0 +1,4 @@ +
      + +
      <%= job['times'] %>
      +
      diff --git a/plugins/jobs_example/schemas/slow_nothing_job.rb b/plugins/jobs_example/schemas/slow_nothing_job.rb new file mode 100644 index 0000000000..a1b5745e1c --- /dev/null +++ b/plugins/jobs_example/schemas/slow_nothing_job.rb @@ -0,0 +1,17 @@ +{ + :schema => { + "$schema" => "http://www.archivesspace.org/archivesspace.json", + "version" => 1, + "type" => "object", + + "properties" => { + + "times" => { + "type" => "string", + "ifmissing" => "error" + } + + + } + } +} diff --git a/plugins/lcnaf/backend/model/lcnaf_converter.rb b/plugins/lcnaf/backend/model/lcnaf_converter.rb index 708bc1bcab..cccf843631 100644 --- a/plugins/lcnaf/backend/model/lcnaf_converter.rb +++ b/plugins/lcnaf/backend/model/lcnaf_converter.rb @@ -22,10 +22,6 @@ def self.instance_for(type, input_file) end end - - def self.profile - "Import all subjects and agents from a MARC XML file, setting source to LCNAF" - end end diff --git a/plugins/lcnaf/frontend/plugin_init.rb b/plugins/lcnaf/frontend/plugin_init.rb index 2c5f9c5c80..9d9cc28819 100644 --- a/plugins/lcnaf/frontend/plugin_init.rb +++ b/plugins/lcnaf/frontend/plugin_init.rb @@ -1,2 +1 @@ -my_routes = [File.join(File.dirname(__FILE__), "routes.rb")] -ArchivesSpace::Application.config.paths['config/routes'].concat(my_routes) +ArchivesSpace::Application.extend_aspace_routes(File.join(File.dirname(__FILE__), "routes.rb")) diff --git a/plugins/public_demo_pugin/README.md b/plugins/public_demo_pugin/README.md new file mode 100644 index 0000000000..a7702ab5e6 --- /dev/null +++ b/plugins/public_demo_pugin/README.md @@ -0,0 +1,18 @@ +ArchivesSpace Public Demo pugin +=============== + +This is an example plugin for the ArchivesSpace public interface +to demonstrate the various plugin hooks. + +It will add pugs. + +## Getting Started + +Enable the plugin by editing the file in `config/config.rb`: + + AppConfig[:plugins] = ['some_plugin', 'public_demo_pugin'] + +This plugin is only compatible with the new public interface +and assumes the environment variable `ASPACE_PUBLIC_NEW` is set: + + ENV['ASPACE_PUBLIC_NEW'] = 'true' \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/assets/pugs.css b/plugins/public_demo_pugin/public/assets/pugs.css new file mode 100644 index 0000000000..9e645ae831 --- /dev/null +++ b/plugins/public_demo_pugin/public/assets/pugs.css @@ -0,0 +1,5 @@ +.pugs-header, +.pugs-footer { + padding: 10px; + overflow: hidden; +} \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/controllers/pugs_controller.rb b/plugins/public_demo_pugin/public/controllers/pugs_controller.rb new file mode 100644 index 0000000000..f5915b0def --- /dev/null +++ b/plugins/public_demo_pugin/public/controllers/pugs_controller.rb @@ -0,0 +1,5 @@ +class PugsController < ApplicationController + def index + @pug_name = "Barry" + end +end diff --git a/plugins/public_demo_pugin/public/locales/en.yml b/plugins/public_demo_pugin/public/locales/en.yml new file mode 100644 index 0000000000..b64aa62159 --- /dev/null +++ b/plugins/public_demo_pugin/public/locales/en.yml @@ -0,0 +1,8 @@ +en: + plugin: + pugs: + footer: PugsSpace - Barry 2016 + header: PugsSpace + menu_label: Pugs + resource_action: URL Pug + archival_object_action: Alert Pug \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/plugin_init.rb b/plugins/public_demo_pugin/public/plugin_init.rb new file mode 100644 index 0000000000..9ff04a38b6 --- /dev/null +++ b/plugins/public_demo_pugin/public/plugin_init.rb @@ -0,0 +1,6 @@ +Plugins::extend_aspace_routes(File.join(File.dirname(__FILE__), "routes.rb")) +Plugins::add_menu_item('/plugin/pugs', 'plugin.pugs.menu_label') +Plugins::add_record_page_action_proc('resource', 'plugin.pugs.resource_action', 'fa-paw', proc {|record| + 'http://example.com/pugs?uri='+record.uri +}, 0) +Plugins::add_record_page_action_js('archival_object', 'plugin.pugs.archival_object_action', 'fa-paw', "alert('PUGS ARE GREAT! URI:'+$(this).data('uri'));", 0) \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/routes.rb b/plugins/public_demo_pugin/public/routes.rb new file mode 100644 index 0000000000..ae5f05c666 --- /dev/null +++ b/plugins/public_demo_pugin/public/routes.rb @@ -0,0 +1,5 @@ +Rails.application.routes.draw do + + get '/plugin/pugs', to: 'pugs#index' + +end diff --git a/plugins/public_demo_pugin/public/views/layout_head.html.erb b/plugins/public_demo_pugin/public/views/layout_head.html.erb new file mode 100644 index 0000000000..ecae390869 --- /dev/null +++ b/plugins/public_demo_pugin/public/views/layout_head.html.erb @@ -0,0 +1,2 @@ +<%# TODO allow for proxy / path settings %> +" media="all" rel="stylesheet" type="text/css"> \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/views/pugs/index.html.erb b/plugins/public_demo_pugin/public/views/pugs/index.html.erb new file mode 100644 index 0000000000..004132d5a0 --- /dev/null +++ b/plugins/public_demo_pugin/public/views/pugs/index.html.erb @@ -0,0 +1,2 @@ +

      Pug: <%= @pug_name %>

      + \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/views/shared/_footer.html.erb b/plugins/public_demo_pugin/public/views/shared/_footer.html.erb new file mode 100644 index 0000000000..8a898e3180 --- /dev/null +++ b/plugins/public_demo_pugin/public/views/shared/_footer.html.erb @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/plugins/public_demo_pugin/public/views/shared/_header.html.erb b/plugins/public_demo_pugin/public/views/shared/_header.html.erb new file mode 100644 index 0000000000..cc86056de6 --- /dev/null +++ b/plugins/public_demo_pugin/public/views/shared/_header.html.erb @@ -0,0 +1,6 @@ +
      +

      + <%= t('plugin.pugs.header') %> + +

      +
      \ No newline at end of file diff --git a/public-new/.gitignore b/public-new/.gitignore index 5b61ab0e2c..7fc91aac08 100644 --- a/public-new/.gitignore +++ b/public-new/.gitignore @@ -1,13 +1,29 @@ -# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# See http://help.github.com/ignore-files/ for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: -# git config --global core.excludesfile '~/.gitignore_global' +# git config --global core.excludesfile ~/.gitignore_global -# Ignore bundler config. +# Ignore bundler config /.bundle +# Ignore the default SQLite database. +/db/*.sqlite3 + # Ignore all logfiles and tempfiles. -/log/* -!/log/.keep +/log/*.log /tmp + +/build/*.jar +/build/gems + +/frontend.war + +/public/assets + +# IDE config folders +/.idea + +# Hide shared tree resources +/vendor/assets/javascripts/archivesspace/largetree.js.erb +/vendor/assets/stylesheets/archivesspace/largetree.scss diff --git a/public-new/Gemfile b/public-new/Gemfile index 6e46383c3f..aae67ec2a2 100644 --- a/public-new/Gemfile +++ b/public-new/Gemfile @@ -1,44 +1,51 @@ source 'https://rubygems.org' -gem 'atomic' - -gem 'json-schema' -gem "net-http-persistent", "2.8" -gem "multipart-post", "1.2.0" - -gem 'puma' - - - -# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.2.4' - -gem 'rails-api' - -group :assets do - gem 'sass-rails', '~> 5.0' - - gem 'uglifier', '>= 1.3.0' - - gem 'compass-rails' - gem 'zurb-foundation' -end - +# Bundle edge Rails +gem 'rails', '5.0.1' +# Use jdbcsqlite3 as the database for Active Record +# gem 'activerecord-jdbcsqlite3-adapter' + +gem "mizuno", "0.6.11" + +# use bootstrap's sass +gem 'bootstrap-sass', '~> 3.3.6' +# Use SCSS for stylesheets +gem 'sass-rails', '5.0.5' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '3.0.4' +# Use font-awesome icons +gem 'font-awesome-sass' + +# not sure if we need to be using Compass.... +# gem 'compass-rails' + +# Use CoffeeScript for .coffee assets and views +gem 'coffee-rails', '4.2.1' +# See https://github.com/rails/execjs#readme for more supported runtimes +gem 'therubyrhino' +# Use jquery as the JavaScript library +gem 'jquery-rails' + +# support clipboard +gem 'clipboard-rails' + +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +gem 'turbolinks', '~> 5' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 2.0' -# bundle exec rake doc:rails generates the API under doc/api. -gem 'sdoc', '~> 0.4.0', group: :doc - +# FIXME? gem 'jbuilder', '~> 2.5' +# Use Redis adapter to run Action Cable in production +# gem 'redis', '~> 3.0' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' -# Use Unicorn as the app server -# gem 'unicorn' - # Use Capistrano for deployment # gem 'capistrano-rails', group: :development +# Enable support for `rails server` +gem 'listen', group: :development # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem 'pry-rails', group: :development diff --git a/public-new/Gemfile.lock b/public-new/Gemfile.lock index b4df64c414..4f517a93f9 100644 --- a/public-new/Gemfile.lock +++ b/public-new/Gemfile.lock @@ -1,165 +1,180 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.4) - actionpack (= 4.2.4) - actionview (= 4.2.4) - activejob (= 4.2.4) + actioncable (5.0.1) + actionpack (= 5.0.1) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.4) - actionview (= 4.2.4) - activesupport (= 4.2.4) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) + actionpack (5.0.1) + actionview (= 5.0.1) + activesupport (= 5.0.1) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.4) - activesupport (= 4.2.4) + actionview (5.0.1) + activesupport (= 5.0.1) builder (~> 3.1) erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.4) - activesupport (= 4.2.4) - globalid (>= 0.3.0) - activemodel (4.2.4) - activesupport (= 4.2.4) - builder (~> 3.1) - activerecord (4.2.4) - activemodel (= 4.2.4) - activesupport (= 4.2.4) - arel (~> 6.0) - activesupport (4.2.4) + activejob (5.0.1) + activesupport (= 5.0.1) + globalid (>= 0.3.6) + activemodel (5.0.1) + activesupport (= 5.0.1) + activerecord (5.0.1) + activemodel (= 5.0.1) + activesupport (= 5.0.1) + arel (~> 7.0) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - arel (6.0.3) - atomic (1.0.1-java) - builder (3.2.2) - chunky_png (1.3.5) - compass (1.0.3) - chunky_png (~> 1.2) - compass-core (~> 1.0.2) - compass-import-once (~> 1.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - sass (>= 3.3.13, < 3.5) - compass-core (1.0.3) - multi_json (~> 1.0) - sass (>= 3.3.0, < 3.5) - compass-import-once (1.0.5) - sass (>= 3.2, < 3.5) - compass-rails (2.0.1) - compass (~> 1.0.0) + arel (7.1.4) + autoprefixer-rails (6.7.1) + execjs + bootstrap-sass (3.3.7) + autoprefixer-rails (>= 5.2.1) + sass (>= 3.3.4) + builder (3.2.3) + clipboard-rails (1.5.16) + coderay (1.1.1) + coffee-rails (4.2.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + concurrent-ruby (1.0.4-java) erubis (2.7.0) - execjs (2.6.0) - ffi (1.9.10-java) - globalid (0.3.6) + execjs (2.7.0) + ffi (1.9.17-java) + font-awesome-sass (4.7.0) + sass (>= 3.2) + globalid (0.3.7) activesupport (>= 4.1.0) i18n (0.7.0) - jbuilder (2.3.2) - activesupport (>= 3.0.0, < 5) - multi_json (~> 1.2) - json (1.8.0-java) - json-schema (1.0.10) + jquery-rails (4.2.2) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.3) - mime-types (>= 1.16, < 3) - mime-types (2.6.2) - minitest (5.8.2) - multi_json (1.11.2) - multipart-post (1.2.0) - net-http-persistent (2.8) - nokogiri (1.6.6.2-java) - puma (2.8.2-java) - rack (>= 1.1, < 2.0) - rack (1.6.4) + mail (2.6.4) + mime-types (>= 1.16, < 4) + method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + minitest (5.10.1) + nio4r (1.2.1-java) + nokogiri (1.7.0.1-java) + pry (0.10.4-java) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + spoon (~> 0.0) + pry-rails (0.3.4) + pry (>= 0.9.10) + puma (3.6.2-java) + rack (2.0.1) rack-test (0.6.3) rack (>= 1.0) - rails (4.2.4) - actionmailer (= 4.2.4) - actionpack (= 4.2.4) - actionview (= 4.2.4) - activejob (= 4.2.4) - activemodel (= 4.2.4) - activerecord (= 4.2.4) - activesupport (= 4.2.4) + rails (5.0.1) + actioncable (= 5.0.1) + actionmailer (= 5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) + activemodel (= 5.0.1) + activerecord (= 5.0.1) + activesupport (= 5.0.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.4) - sprockets-rails - rails-api (0.4.0) - actionpack (>= 3.2.11) - railties (>= 3.2.11) - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) - activesupport (>= 4.2.0.beta, < 5.0) - nokogiri (~> 1.6.0) - rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.2) + railties (= 5.0.1) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.2) + activesupport (>= 4.2.0, < 6.0) + nokogiri (~> 1.6) + rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.4) - actionpack (= 4.2.4) - activesupport (= 4.2.4) + railties (5.0.1) + actionpack (= 5.0.1) + activesupport (= 5.0.1) + method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.2.2) - rb-fsevent (0.9.6) - rb-inotify (0.9.5) + rake (12.0.0) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) ffi (>= 0.5.0) - rdoc (4.2.0) - json (~> 1.4) - sass (3.4.19) - sass-rails (5.0.4) - railties (>= 4.0.0, < 5.0) + ruby_dep (1.5.0) + sass (3.4.23) + sass-rails (5.0.5) + railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sdoc (0.4.1) - json (~> 1.7, >= 1.7.7) - rdoc (~> 4.0) - sprockets (3.4.0) + slop (3.6.0) + spoon (0.0.6) + ffi + sprockets (3.7.1) + concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) - thor (0.19.1) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + therubyrhino (2.0.4) + therubyrhino_jar (>= 1.7.3) + therubyrhino_jar (1.7.6) + thor (0.19.4) thread_safe (0.3.5-java) - tilt (2.0.1) + tilt (2.0.6) + turbolinks (5.0.1) + turbolinks-source (~> 5) + turbolinks-source (5.0.0) tzinfo (1.2.2) thread_safe (~> 0.1) - tzinfo-data (1.2015.7) + tzinfo-data (1.2016.10) tzinfo (>= 1.0.0) - uglifier (2.7.2) - execjs (>= 0.3.0) - json (>= 1.8.0) - zurb-foundation (4.3.2) - sass (>= 3.2.0) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) + websocket-driver (0.6.5-java) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) PLATFORMS java DEPENDENCIES - atomic - compass-rails - jbuilder (~> 2.0) - json-schema - multipart-post (= 1.2.0) - net-http-persistent (= 2.8) - puma - rails (= 4.2.4) - rails-api - sass-rails (~> 5.0) - sdoc (~> 0.4.0) + bootstrap-sass (~> 3.3.6) + clipboard-rails + coffee-rails (= 4.2.1) + font-awesome-sass + jquery-rails + listen + pry-rails + puma (= 3.6.2) + rails (= 5.0.1) + sass-rails (= 5.0.5) + therubyrhino + turbolinks (~> 5) tzinfo-data - uglifier (>= 1.3.0) - zurb-foundation + uglifier (= 3.0.4) BUNDLED WITH - 1.10.6 + 1.12.5 diff --git a/public-new/ISSUES.txt b/public-new/ISSUES.txt deleted file mode 100644 index 8c90f34589..0000000000 --- a/public-new/ISSUES.txt +++ /dev/null @@ -1,60 +0,0 @@ -X No 404 when the router doesn't catch the URL - -X Back button causes overlay to hang - -X Need better overlay graphic - -X Search result row template has dummy data - * "Summary" field has dummy data - -* Need Record pages for each type - X Record Model / Endpoint - X Tests - * Templating - -X Need to catch the click events in the navbar - -* Don't show Next and Previous if there's only 1 page - -* Sorting dropdown doesn't work - -* Dates filter doesn't work - -* Breadcrumb doesn't work - -* Display options tabs don't work - -* Facets need a "see more" link over 20 or so - -X Keywords in context doesn't work - X widget - * see more link isn't functional - -* Error handling - * server error: search, record - * timeout - X 404 - -X Revise Search Functionality - -* Returning to / using back button doesn't clear search results view - -* How to make the back button restore the search query form state.. - -* When user toggles the "Revise Search" tools, and changes the state of the dropdowns, what should happen if she changes a settings that don't affect the query, such as pageSize? - - discard the "Revise Search" state? - - try to apply the revisions and the page size change? - - not possible because "Revise Search" disables other controls? - -* Solr fields of type 'string' will only match if the entire string - matches. Therefore, they won't show up in highlights. For example, - finding_aid_title. What would happen if this were changed to type - 'text_general'? - * Similar - 'fullrecord' is indexed but not stored, therefore doesn't - appear in highlight snippets. Maybe solve this by cleaning out the structural stuff and storing the field. Or maybe add a second lightweight version that gets stored. - -* API appears to ignore search rows after 3. - -X Revise Search toggle gets funked if the search url is the same - -* Sort dropdown not working diff --git a/public-new/README.md b/public-new/README.md index 932933e947..38e42cc7d4 100644 --- a/public-new/README.md +++ b/public-new/README.md @@ -1,49 +1,93 @@ -ArchivesSpace Public UI Development Branch -==================================== +# Running a devserver using JRuby -This is a development project to replace the ArchivesSpace Public UI in Spring 2016. +Check out this repository, then: -# Overview + # Run the ArchivesSpace devserver on port 4567 + cd /path/to/archivesspace; build/run devserver -You can make the `public:devserver` ant task point to this application by doing `export ASPACE_PUBLIC_DEV=true`. + # Now run the PUI application's devserver + cd pui-checkout-dir -To get the development server running using the standard development build tools, do this: + # Download JRuby and gems + build/run bootstrap -* Open two terminal windows -* In each window: + # Run the devserver listening on port 4000 + build/run devserver - `cd archivesspace` - `export ASPACE_PUBLIC_DEV=true` +If you prefer MRI Ruby, it should run using that too. You might just +need to remove `Gemfile.lock` prior to running bundler to install the +gems. Maybe there's a way we can get these to peacefully coexist... -* In window 1: - `./build/run boostrap` - `./build/run backend:devserver` +# Using Pry -* In window 2: - `./build/run public:devserver` +One disadvantage of launching the devserver from Ant is that it messes +with your console, disabling input echo. If you're trying to use +interactive tools like Pry, that's a bit of an inconvenience. -Point your browser to `http://localhost:3001` +To get around this, running `build/run devserver` will also write out +a shell script (to `build/devserver.sh`) that captures the command, +working directory and environment variables needed to run a +devserver. As long as you run `build/run devserver` once, you can run +`build/devserver.sh` thereafter to have a more normal console +experience. +# Configuration and Text Customization -# Development Notes +At the top-level of this project, there is a configuration file called +`config/config.rb` whose format matches that of ArchivesSpace. Here +you can add (or override) configuration options for your local +install. -See the `README_BOWER.md` file in the `frontend` for guidelines on managing frontend assets. +To see the full list of available options, see the file +[`app/archivesspace-public/app/lib/config_defaults.rb`](app/archivesspace-public/app/lib/config_defaults.rb) -Unlike existing ASpace applications, this app uses Rails 4. +See the [`config/config.rb.example`](config/config.rb.example) file for implementation examples. -## Javascript +In addition, you can override some default text values found in [`app/archivesspace-public/config/locales`](app/archivesspace-public/config/locales) -- for example, the site title -- by creating an +`app/archivesspace-public/config/custom/locales` directory, and placing the appropriate `.yml` files[s] there. -The javascript layer currently uses Exoskeleton (a drop-in replacement for BackboneJS), Jquery, and Lodash. +## Preserving Patron Privacy -## CSS +The **:block_referrer** key in the configuration file (default: **true**) determines whether the full referring URL is +transmitted when the user clicks a link to a website outside the web domain of your instance of ArchivesSpace. This +protects your patrons from tracking by that site. -The CSS layer will probably use Foundation CSS or Twitter Bootstrap. +## Main Navigation Menu +You can choose not to display one or more of the links on the main (horizontal) navigation menu, +either globally or by repository, if you have more than one repository. You manage this through the +`config/config.rb` file; [`config/config.rb.example`](config/config.rb.example) shows examples of these. -# Contributing +## Repository Customization -Yes, please do it - especially if you are a master of CSS and HTML with an interest in graphic design. You can sign up by adding your name and how you'd like to contribute to this list and submitting a pull request. +### Display of "badges" on the Repository page -Yes, I'd like to contribute: +You can configure which badges appear on the Repository page, both globally or by repository. Again, +[`config/config.rb.example`](config/config.rb.example) shows examples. -Name Notes +### Addition of a "lead paragraph" + +You can also use the custom `.yml` files, described above, to add a custom "lead paragraph" (including html markup) for one or more of your repositories, keyed to the repository's code. + +For example, if your repository, `My Wonderful Repository` has a code of `MWR`, this is what you might see in the +custom `en.yml`: +``` +en: + repos: + mwr: + lead_graph: This amazing repository has so much to offer you! +``` + + +## Activation of the "Request" button on archival object pages + +You can configure, both globally or by repository, whether the "Request" button is active on +archival object pages for objects that don't have an associated Top Container. +See [`config/config.rb.example`](config/config.rb.example) for examples. + + +## License + +ArchivesSpace is released under the [Educational Community License, +version 2.0](http://opensource.org/licenses/ecl2.php). See the +[COPYING](COPYING) file for more information. diff --git a/public-new/README_JASMINE.md b/public-new/README_JASMINE.md deleted file mode 100644 index adb9444a07..0000000000 --- a/public-new/README_JASMINE.md +++ /dev/null @@ -1,10 +0,0 @@ -Unit Testing Frontend Assets with Jasmine -==================================== - -This is a proof of concept and is under development. - -To run JS Unit tests, install NPM and: - - cd archivesspace/public-new - npm install - $(npm bin)/karma start jasmine/my.conf.js --single-run diff --git a/public-new/Rakefile b/public-new/Rakefile index ba6b733dd2..e85f913914 100644 --- a/public-new/Rakefile +++ b/public-new/Rakefile @@ -1,6 +1,6 @@ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require_relative 'config/application' Rails.application.load_tasks diff --git a/public-new/app/assets/config/manifest.js b/public-new/app/assets/config/manifest.js new file mode 100644 index 0000000000..b16e53d6d5 --- /dev/null +++ b/public-new/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/public-new/app/assets/fonts/PTS55F-webfont.eot b/public-new/app/assets/fonts/PTS55F-webfont.eot new file mode 100644 index 0000000000..d838711c19 Binary files /dev/null and b/public-new/app/assets/fonts/PTS55F-webfont.eot differ diff --git a/public-new/app/assets/fonts/PTS55F-webfont.svg b/public-new/app/assets/fonts/PTS55F-webfont.svg new file mode 100644 index 0000000000..919880c9b0 --- /dev/null +++ b/public-new/app/assets/fonts/PTS55F-webfont.svg @@ -0,0 +1,2743 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public-new/app/assets/fonts/PTS55F-webfont.ttf b/public-new/app/assets/fonts/PTS55F-webfont.ttf new file mode 100644 index 0000000000..48b46372a6 Binary files /dev/null and b/public-new/app/assets/fonts/PTS55F-webfont.ttf differ diff --git a/public-new/app/assets/fonts/PTS55F-webfont.woff b/public-new/app/assets/fonts/PTS55F-webfont.woff new file mode 100644 index 0000000000..05d8c1ab90 Binary files /dev/null and b/public-new/app/assets/fonts/PTS55F-webfont.woff differ diff --git a/public-new/app/assets/fonts/PTS56F-webfont.eot b/public-new/app/assets/fonts/PTS56F-webfont.eot new file mode 100644 index 0000000000..c37ab76371 Binary files /dev/null and b/public-new/app/assets/fonts/PTS56F-webfont.eot differ diff --git a/public-new/app/assets/fonts/PTS56F-webfont.svg b/public-new/app/assets/fonts/PTS56F-webfont.svg new file mode 100644 index 0000000000..e3946a2e0d --- /dev/null +++ b/public-new/app/assets/fonts/PTS56F-webfont.svg @@ -0,0 +1,1945 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public-new/app/assets/fonts/PTS56F-webfont.ttf b/public-new/app/assets/fonts/PTS56F-webfont.ttf new file mode 100644 index 0000000000..d392b05ce0 Binary files /dev/null and b/public-new/app/assets/fonts/PTS56F-webfont.ttf differ diff --git a/public-new/app/assets/fonts/PTS56F-webfont.woff b/public-new/app/assets/fonts/PTS56F-webfont.woff new file mode 100644 index 0000000000..e6c40a9151 Binary files /dev/null and b/public-new/app/assets/fonts/PTS56F-webfont.woff differ diff --git a/public-new/app/assets/fonts/PTS75F-webfont.eot b/public-new/app/assets/fonts/PTS75F-webfont.eot new file mode 100644 index 0000000000..ed65f7cd61 Binary files /dev/null and b/public-new/app/assets/fonts/PTS75F-webfont.eot differ diff --git a/public-new/app/assets/fonts/PTS75F-webfont.svg b/public-new/app/assets/fonts/PTS75F-webfont.svg new file mode 100644 index 0000000000..0892a98738 --- /dev/null +++ b/public-new/app/assets/fonts/PTS75F-webfont.svg @@ -0,0 +1,3025 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public-new/app/assets/fonts/PTS75F-webfont.ttf b/public-new/app/assets/fonts/PTS75F-webfont.ttf new file mode 100644 index 0000000000..2e048ea24a Binary files /dev/null and b/public-new/app/assets/fonts/PTS75F-webfont.ttf differ diff --git a/public-new/app/assets/fonts/PTS75F-webfont.woff b/public-new/app/assets/fonts/PTS75F-webfont.woff new file mode 100644 index 0000000000..ec40248d04 Binary files /dev/null and b/public-new/app/assets/fonts/PTS75F-webfont.woff differ diff --git a/public-new/app/assets/fonts/PTS76F-webfont.eot b/public-new/app/assets/fonts/PTS76F-webfont.eot new file mode 100644 index 0000000000..c586752f97 Binary files /dev/null and b/public-new/app/assets/fonts/PTS76F-webfont.eot differ diff --git a/public-new/app/assets/fonts/PTS76F-webfont.svg b/public-new/app/assets/fonts/PTS76F-webfont.svg new file mode 100644 index 0000000000..10ee83b124 --- /dev/null +++ b/public-new/app/assets/fonts/PTS76F-webfont.svg @@ -0,0 +1,2599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public-new/app/assets/fonts/PTS76F-webfont.ttf b/public-new/app/assets/fonts/PTS76F-webfont.ttf new file mode 100644 index 0000000000..e7477bcae5 Binary files /dev/null and b/public-new/app/assets/fonts/PTS76F-webfont.ttf differ diff --git a/public-new/app/assets/fonts/PTS76F-webfont.woff b/public-new/app/assets/fonts/PTS76F-webfont.woff new file mode 100644 index 0000000000..64f79c9ec8 Binary files /dev/null and b/public-new/app/assets/fonts/PTS76F-webfont.woff differ diff --git a/public-new/app/assets/fonts/README.md b/public-new/app/assets/fonts/README.md new file mode 100644 index 0000000000..2f0a0cf8ba --- /dev/null +++ b/public-new/app/assets/fonts/README.md @@ -0,0 +1,35 @@ +# The fonts used in this application + +The fonts used in this application are + +* PT Sans +* Roboto Slab + +They have been downloaded from [Font Squirrel] (https://www.fontsquirrel.com/), whose license is as follows: + +Copyright © 2009 ParaType Ltd. +with Reserved Names &quot;PT Sans&quot; and &quot;ParaType&quot;. + +FONT LICENSE + +PERMISSION &amp; CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the font software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the font software, subject to the following conditions: + +1) Neither the font software nor any of its individual components, in original or modified versions, may be sold by itself. + +2) Original or modified versions of the font software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No modified version of the font software may use the Reserved Name(s) or combinations of Reserved Names with other words unless explicit written permission is granted by the ParaType. This restriction only applies to the primary font name as presented to the users. + +4) The name of ParaType or the author(s) of the font software shall not be used to promote, endorse or advertise any modified version, except to acknowledge the contribution(s) of ParaType and the author(s) or with explicit written permission of ParaType. + +5) The font software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION &amp; TERRITORY +This license has no limits on time and territory, but it becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL PARATYPE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +ParaType Ltd +http://www.paratype.ru \ No newline at end of file diff --git a/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.eot b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.eot new file mode 100644 index 0000000000..004ae6fd0e Binary files /dev/null and b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.eot differ diff --git a/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.svg b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.svg new file mode 100644 index 0000000000..d205f1e4ad --- /dev/null +++ b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.svg @@ -0,0 +1,8554 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.ttf b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.ttf new file mode 100644 index 0000000000..070f48a494 Binary files /dev/null and b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.ttf differ diff --git a/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.woff b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.woff new file mode 100644 index 0000000000..15604c37ba Binary files /dev/null and b/public-new/app/assets/fonts/RobotoSlab-Bold-webfont.woff differ diff --git a/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.eot b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.eot new file mode 100644 index 0000000000..be8f5ca426 Binary files /dev/null and b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.eot differ diff --git a/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.svg b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.svg new file mode 100644 index 0000000000..e55df1c2c0 --- /dev/null +++ b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.svg @@ -0,0 +1,8176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.ttf b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.ttf new file mode 100644 index 0000000000..fc7cdabbb0 Binary files /dev/null and b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.ttf differ diff --git a/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.woff b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.woff new file mode 100644 index 0000000000..a5f89db126 Binary files /dev/null and b/public-new/app/assets/fonts/RobotoSlab-Regular-webfont.woff differ diff --git a/public-new/app/assets/javascripts/accordion-table.js b/public-new/app/assets/javascripts/accordion-table.js deleted file mode 100644 index 8f2c4cdd7a..0000000000 --- a/public-new/app/assets/javascripts/accordion-table.js +++ /dev/null @@ -1,252 +0,0 @@ -/** - * Accordion module. - * @module foundation.accordion - * @requires foundation.util.keyboard - * @requires foundation.util.motion - */ -!function($, Foundation) { - 'use strict'; - - /** - * Creates a new instance of an accordion. - * @class - * @fires Accordion#init - * @param {jQuery} element - jQuery object to make into an accordion. - */ - function AccordionTable(element, options){ - this.$element = element; - this.options = $.extend({}, AccordionTable.defaults, this.$element.data(), options); - - this._init(); - - Foundation.registerPlugin(this, 'AccordionTable'); - Foundation.Keyboard.register('AccordionTable', { - 'ENTER': 'toggle', - 'SPACE': 'toggle', - 'ARROW_DOWN': 'next', - 'ARROW_UP': 'previous' - }); - } - - AccordionTable.defaults = { - /** - * Amount of time to animate the opening of an accordion pane. - * @option - * @example 250 - */ - slideSpeed: 250, - /** - * Allow the accordion to have multiple open panes. - * @option - * @example false - */ - multiExpand: false, - /** - * Allow the accordion to close all panes. - * @option - * @example false - */ - allowAllClosed: false - }; - - /** - * Initializes the accordion by animating the preset active pane(s). - * @private - */ - AccordionTable.prototype._init = function() { - // this.$element.attr('role', 'tablist'); - // this.$tabs = this.$element.children('li'); - this.$navs = this.$element.children('.navigation-row'); - - // if (this.$tabs.length == 0) { - // this.$tabs = this.$element.children('[data-accordion-item]'); - // } - this.$navs.each(function(idx, el){ - - var $el = $(el); - var id = $el.data('header-for'); - var $content = $el.next('#'+id); - // $content = $el.find('[data-tab-content]'), - // id = $content[0].id || Foundation.GetYoDigits(6, 'accordion'), - var linkId = el.id || id + '-label'; - - $el.attr({ - 'aria-controls': id, - 'role': 'tab', - 'id': linkId, - 'aria-expanded': false, - 'aria-selected': false - }); - $content.attr({'role': 'tabpanel', 'aria-labelledby': linkId, 'aria-hidden': true, 'id': id}); - }); - var $initActive = this.$element.find('.is-active').children('[data-tab-content]'); - if($initActive.length){ - this.down($initActive, true); - } - this._events(); - }; - - /** - * Adds event handlers for items within the accordion. - * @private - */ - AccordionTable.prototype._events = function() { - var _this = this; - - this.$navs.each(function(){ - var $elem = $(this); - var id = $elem.data('header-for'); - var $content = $elem.next('#'+id); - if ($content.length) { - $elem.off('click.zf.accordionTable keydown.zf.accordionTable') - .on('click.zf.accordionTable', function(e){ - // ignore hyperlinks in nav rows - if(e.target.tagName.toLowerCase() === 'a') - return; - // $(this).children('a').on('click.zf.accordion', function(e) { - e.preventDefault(); - if ($elem.hasClass('is-active')) { - console.log("click up"); - // if(_this.options.allowAllClosed || $elem.siblings().hasClass('is-active')){ - _this.up($content); - // } - } - else { - console.log("click down"); - _this.down($content); - } - }).on('keydown.zf.accordionTable', function(e){ - console.log("on.keydown.accordionTable"); - Foundation.Keyboard.handleKey(e, 'AccordionTable', { - toggle: function() { - _this.toggle($content); - }, - next: function() { - $elem.next().focus().trigger('click.zf.accordion'); - }, - previous: function() { - $elem.prev().focus().trigger('click.zf.accordion'); - }, - handled: function() { - e.preventDefault(); - e.stopPropagation(); - } - }); - }); - } - }); - }; - /** - * Toggles the selected content pane's open/close state. - * @param {jQuery} $target - jQuery object of the pane to toggle. - * @function - */ - AccordionTable.prototype.toggle = function($target){ - if($target.parent().hasClass('is-active')){ - if(this.options.allowAllClosed || $target.parent().siblings().hasClass('is-active')){ - this.up($target); - }else{ return; } - }else{ - console.log("toggle down"); - this.down($target); - } - }; - /** - * Opens the accordion tab defined by `$target`. - * @param {jQuery} $target - Accordion pane to open. - * @param {Boolean} firstTime - flag to determine if reflow should happen. - * @fires Accordion#down - * @function - */ - AccordionTable.prototype.down = function($target, firstTime) { - console.log("down"); - var _this = this; - if(!this.options.multiExpand && !firstTime){ - var $currentActive = this.$element.find('.is-active').next('.content-row'); - if($currentActive.length){ - this.up($currentActive); - } - } - - $target - .attr('aria-hidden', false) - .attr('style', 'display: table-row;') - .parent('[data-tab-content]') - .addBack() - .prev('.navigation-row').addClass('is-active'); - - // $target.slideDown(_this.options.slideSpeed); - - - // if(!firstTime){ - // Foundation._reflow(this.$element.attr('data-accordion')); - // } - var $navEl = $('#' + $target.attr('aria-labelledby')); - $navEl.attr({ - 'aria-expanded': true, - 'aria-selected': true - }); - - $('div:first-child div', $navEl).removeClass("arrow-right").addClass("arrow-down"); - - /** - * Fires when the tab is done opening. - * @event Accordion#down - */ - this.$element.trigger('down.zf.accordion', [$target]); - }; - - /** - * Closes the tab defined by `$target`. - * @param {jQuery} $target - Accordion tab to close. - * @fires Accordion#up - * @function - */ - AccordionTable.prototype.up = function($target) { - console.log("up"); - var $aunts = $target.prev('.navigation-row').siblings('.navigation-row'), - _this = this; - // var canClose = this.options.multiExpand ? $aunts.hasClass('is-active') : $target.parent().hasClass('is-active'); - - // if(!this.options.allowAllClosed && !canClose){ - // return; - // } - - $target - .attr('style', 'display: none;'); - - // $target.slideUp(_this.options.slideSpeed); - - $target.attr('aria-hidden', true) - .prev('.navigation-row').removeClass('is-active'); - - var $navEl = $('#' + $target.attr('aria-labelledby')); - $navEl.attr({ - 'aria-expanded': false, - 'aria-selected': false - }); - - $('div:first-child div', $navEl).removeClass("arrow-down").addClass("arrow-right"); - - - /** - * Fires when the tab is done collapsing up. - * @event Accordion#up - */ - this.$element.trigger('up.zf.accordion', [$target]); - }; - - /** - * Destroys an instance of an accordion. - * @fires Accordion#destroyed - * @function - */ - AccordionTable.prototype.destroy = function() { - this.$element.find('[data-tab-content]').slideUp(0).css('display', ''); - this.$element.find('a').off('.zf.accordion'); - - Foundation.unregisterPlugin(this); - }; - - Foundation.plugin(AccordionTable, 'AccordionTable'); -}(jQuery, window.Foundation); diff --git a/public-new/app/assets/javascripts/agents.js b/public-new/app/assets/javascripts/agents.js deleted file mode 100644 index e4f0f38bac..0000000000 --- a/public-new/app/assets/javascripts/agents.js +++ /dev/null @@ -1,155 +0,0 @@ -var app = app || {}; -(function(Bb, _, $) { - - function formatName(name) { - var result = "" - if(name.rest_of_name) { - result = result+name.rest_of_name + " "; - } - result = result + name.primary_name; - if(name.dates) { - result = result+" (Dates: "+name.dates+")"; - } - - return result; - } - - - function AgentPresenter(model) { - app.AbstractRecordPresenter.call(this, model); - - var nameList = "
        "; - _.forEach(model.attributes.names, function(name) { - nameList = nameList+"
      • "+formatName(name)+"
      • "; - }); - nameList = nameList + "
      " - - this.nameList = nameList; - - var relations = {} - - _.forEach(model.attributes.related_agents, function(agent_link) { - if(!relations[agent_link['relator']]) - relations[agent_link['relator']] = []; - - relations[agent_link['relator']].push(agent_link._resolved.display_name.sort_name); - }) - - this.relatedAgents = relations; - - if(model.attributes.external_documents && model.attributes.external_documents.length) { - this.externalDocuments = "
        "+_.map(model.attributes.external_documents, function(doc) { - return "
      • "+doc.title+"
      • "; - }).join('') + "
          "; - } - - if(model.attributes.rights_statements) { - this.rightsStatements = _.map(model.attributes.rights_statements, function(statement) { - return app.utils.formatRightsStatement(statement); - }); - } - } - - AgentPresenter.prototype = Object.create(app.AbstractRecordPresenter.prototype); - AgentPresenter.prototype.constructor = AgentPresenter; - - - var AgentModel = Bb.Model.extend({ - initialize: function(opts) { - this.recordType = opts.asType || app.utils.getASType(opts.type); - this.id = opts.id - return this; - }, - - url: function() { - var url = RAILS_API; - - switch(this.recordType) { - case 'agent_person': - url = url + "/people/" + this.id; - break; - } - - return url; - } - - }); - - - var AgentContainerView = Bb.View.extend({ - el: "#container", - - initialize: function(opts) { - this.model = new AgentModel(opts); - var $el = this.$el; - - this.on("recordloaded.aspace", function(model) { - var presenter = new AgentPresenter(model); - app.debug = {}; - app.debug.model = model; - app.debug.presenter = presenter; - - $el.html(app.utils.tmpl('record', presenter)); - $('.abstract', $el).readmore(300); - - var embeddedSearchView = new app.EmbeddedSearchView({ - filters: [{"agent_uris": presenter.uri}], - sortKey: presenter.uri.replace(/\//g, '_')+"_relator_sort asc" - }); - - var nameSidebarView = new NameSidebarView({ - presenter: presenter - }); - - }); - - this.render(); - }, - - render: function() { - var that = this; - var model = this.model; - - $('#wait-modal').foundation('open'); - - this.model.fetch().then(function() { - that.trigger("recordloaded.aspace", model); - }).fail(function(response) { - var errorView = new app.ServerErrorView({ - response: response - }); - - that.$el.html(errorView.$el.html()); - }).always(function() { - setTimeout(function() { - try { - $('#wait-modal').foundation('close'); - $('#container').foundation(); - } catch(e) { - } - }, 500); - }); - } - }); - - - var NameSidebarView = Bb.View.extend({ - el: "#sidebar-container", - - initialize: function(opts) { - this.presenter = opts.presenter; - this.render(); - }, - - render: function() { - this.$el.addClass('name-sidebar'); - this.$el.html(app.utils.tmpl('more-about-name', this.presenter, true)); - - this.$el.foundation(); - } - - }); - - app.AgentContainerView = AgentContainerView; - -})(Backbone, _, jQuery); diff --git a/public-new/app/assets/javascripts/application.js b/public-new/app/assets/javascripts/application.js index a1a74ccb92..83a43819c3 100644 --- a/public-new/app/assets/javascripts/application.js +++ b/public-new/app/assets/javascripts/application.js @@ -1,35 +1,7 @@ -//= require lodash/lodash -//= require lodash.aspace -//= require lodash-inflection/lodash-inflection -//= require jquery/jquery -//= require exoskeleton/exoskeleton -//= require backbone.paginator/backbone.paginator +//= require jquery +//= require jquery_ujs +//= require bootstrap-sprockets //= require jquery.scrollTo/jquery.scrollTo -//= require foundation-sites/foundation.core -//= require foundation-sites/foundation.util.keyboard -//= require foundation-sites/foundation.util.box -//= require foundation-sites/foundation.util.triggers -//= require foundation-sites/foundation.util.mediaQuery -//= require foundation-sites/foundation.util.motion -//= require foundation-sites/foundation.reveal -//= require foundation-sites/foundation.dropdown -//= require foundation-sites/foundation.accordion -//= require jstree/jstree -//= require readmore -//= require accordion-table -//= require record-presenter -//= require utils -//= require icons -//= require router -//= require main -//= require search-results -//= require search-editor -//= require searching -//= require embedded-search -//= require records -//= require agents -//= require subjects -//= require repositories -//= require classifications -//= require resource-tree-sidebar -//= require welcome +//= require lodash/lodash +//= require clipboard +//= require_tree . diff --git a/public-new/app/assets/javascripts/bootstrap-accessibility/bootstrap-accessibility.js b/public-new/app/assets/javascripts/bootstrap-accessibility/bootstrap-accessibility.js new file mode 100644 index 0000000000..15b82f5127 --- /dev/null +++ b/public-new/app/assets/javascripts/bootstrap-accessibility/bootstrap-accessibility.js @@ -0,0 +1,716 @@ +/* ======================================================================== +* Extends Bootstrap v3.1.1 + +* Copyright (c) <2015> PayPal + +* All rights reserved. + +* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +* Neither the name of PayPal or any of its subsidiaries or affiliates nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +* ======================================================================== */ + + + (function($) { + "use strict"; + + // GENERAL UTILITY FUNCTIONS + // =============================== + + var uniqueId = function(prefix) { + return (prefix || 'ui-id') + '-' + Math.floor((Math.random()*1000)+1) + } + + + var removeMultiValAttributes = function (el, attr, val) { + var describedby = (el.attr( attr ) || "").split( /\s+/ ) + , index = $.inArray(val, describedby) + if ( index !== -1 ) { + describedby.splice( index, 1 ) + } + describedby = $.trim( describedby.join( " " ) ) + if (describedby ) { + el.attr( attr, describedby ) + } else { + el.removeAttr( attr ) + } + } + +// selectors Courtesy: https://github.com/jquery/jquery-ui/blob/master/ui/focusable.js and tabbable.js +/* +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-ui + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code contained within the demos directory. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== +*/ + + var focusable = function ( element, isTabIndexNotNaN ) { + var map, mapName, img, + nodeName = element.nodeName.toLowerCase(); + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap='#" + mapName + "']" )[ 0 ]; + return !!img && visible( img ); + } + return ( /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + "a" === nodeName ? + element.href || isTabIndexNotNaN :isTabIndexNotNaN) && visible( element ); // the element and all of its ancestors must be visible + } + var visible = function ( element ) { + return $.expr.filters.visible( element ) && + !$( element ).parents().addBack().filter(function() { + return $.css( this, "visibility" ) === "hidden"; + }).length; + } + + $.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo(function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, + + focusable: function( element ) { + return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); + }, + + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + isTabIndexNaN = isNaN( tabIndex ); + return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); + } + }); + + // Modal Extension + // =============================== + + $('.modal-dialog').attr( {'role' : 'document'}) + var modalhide = $.fn.modal.Constructor.prototype.hide + $.fn.modal.Constructor.prototype.hide = function(){ + modalhide.apply(this, arguments) + $(document).off('keydown.bs.modal') + } + + var modalfocus = $.fn.modal.Constructor.prototype.enforceFocus + $.fn.modal.Constructor.prototype.enforceFocus = function(){ + var $content = this.$element.find(".modal-content") + var focEls = $content.find(":tabbable") + , $lastEl = $(focEls[focEls.length-1]) + , $firstEl = $(focEls[0]) + $lastEl.on('keydown.bs.modal', $.proxy(function (ev) { + if(ev.keyCode === 9 && !(ev.shiftKey | ev.ctrlKey | ev.metaKey | ev.altKey)) { // TAB pressed + ev.preventDefault(); + $firstEl.focus(); + } + }, this)) + $firstEl.on('keydown.bs.modal', $.proxy(function (ev) { + if(ev.keyCode === 9 && ev.shiftKey) { // SHIFT-TAB pressed + ev.preventDefault(); + $lastEl.focus(); + } + }, this)) + modalfocus.apply(this, arguments) + } + + // DROPDOWN Extension + // =============================== + + var toggle = '[data-toggle=dropdown]' + , $par + , firstItem + , focusDelay = 200 + , menus = $(toggle).parent().find('ul').attr('role','menu') + , lis = menus.find('li').attr('role','presentation') + + // add menuitem role and tabIndex to dropdown links + lis.find('a').attr({'role':'menuitem', 'tabIndex':'-1'}) + // add aria attributes to dropdown toggle + $(toggle).attr({ 'aria-haspopup':'true', 'aria-expanded': 'false'}) + + $(toggle).parent() + // Update aria-expanded when open + .on('shown.bs.dropdown',function(e){ + $par = $(this) + var $toggle = $par.find(toggle) + $toggle.attr('aria-expanded','true') + $toggle.on('keydown.bs.dropdown', $.proxy(function (ev) { + setTimeout(function() { + firstItem = $('.dropdown-menu [role=menuitem]:visible', $par)[0] + try{ firstItem.focus()} catch(ex) {} + }, focusDelay) + }, this)) + + }) + // Update aria-expanded when closed + .on('hidden.bs.dropdown',function(e){ + $par = $(this) + var $toggle = $par.find(toggle) + $toggle.attr('aria-expanded','false') + }) + + // Close the dropdown if tabbed away from + $(document) + .on('focusout.dropdown.data-api', '.dropdown-menu', function(e){ + var $this = $(this) + , that = this; + // since we're trying to close when appropriate, + // make sure the dropdown is open + if (!$this.parent().hasClass('open')) { + return; + } + setTimeout(function() { + if(!$.contains(that, document.activeElement)){ + $this.parent().find('[data-toggle=dropdown]').dropdown('toggle') + } + }, 150) + }) + .on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , $.fn.dropdown.Constructor.prototype.keydown); + + // Tab Extension + // =============================== + + var $tablist = $('.nav-tabs, .nav-pills') + , $lis = $tablist.children('li') + , $tabs = $tablist.find('[data-toggle="tab"], [data-toggle="pill"]') + + if($tabs){ + $tablist.attr('role', 'tablist') + $lis.attr('role', 'presentation') + $tabs.attr('role', 'tab') + } + + $tabs.each(function( index ) { + var tabpanel = $($(this).attr('href')) + , tab = $(this) + , tabid = tab.attr('id') || uniqueId('ui-tab') + + tab.attr('id', tabid) + + if(tab.parent().hasClass('active')){ + tab.attr( { 'tabIndex' : '0', 'aria-selected' : 'true', 'aria-controls': tab.attr('href').substr(1) } ) + tabpanel.attr({ 'role' : 'tabpanel', 'tabIndex' : '0', 'aria-hidden' : 'false', 'aria-labelledby':tabid }) + }else{ + tab.attr( { 'tabIndex' : '-1', 'aria-selected' : 'false', 'aria-controls': tab.attr('href').substr(1) } ) + tabpanel.attr( { 'role' : 'tabpanel', 'tabIndex' : '-1', 'aria-hidden' : 'true', 'aria-labelledby':tabid } ) + } + }) + + $.fn.tab.Constructor.prototype.keydown = function (e) { + var $this = $(this) + , $items + , $ul = $this.closest('ul[role=tablist] ') + , index + , k = e.which || e.keyCode + + $this = $(this) + if (!/(37|38|39|40)/.test(k)) return + + $items = $ul.find('[role=tab]:visible') + index = $items.index($items.filter(':focus')) + + if (k == 38 || k == 37) index-- // up & left + if (k == 39 || k == 40) index++ // down & right + + + if(index < 0) index = $items.length -1 + if(index == $items.length) index = 0 + + var nextTab = $items.eq(index) + if(nextTab.attr('role') ==='tab'){ + + nextTab.tab('show') //Comment this line for dynamically loaded tabPabels, to save Ajax requests on arrow key navigation + .focus() + } + // nextTab.focus() + + e.preventDefault() + e.stopPropagation() + } + + $(document).on('keydown.tab.data-api','[data-toggle="tab"], [data-toggle="pill"]' , $.fn.tab.Constructor.prototype.keydown) + + var tabactivate = $.fn.tab.Constructor.prototype.activate; + $.fn.tab.Constructor.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + $active.find('[data-toggle=tab], [data-toggle=pill]').attr({ 'tabIndex' : '-1','aria-selected' : false }) + $active.filter('.tab-pane').attr({ 'aria-hidden' : true,'tabIndex' : '-1' }) + + tabactivate.apply(this, arguments) + + element.addClass('active') + element.find('[data-toggle=tab], [data-toggle=pill]').attr({ 'tabIndex' : '0','aria-selected' : true }) + element.filter('.tab-pane').attr({ 'aria-hidden' : false,'tabIndex' : '0' }) + } + + // Collapse Extension + // =============================== + + var $colltabs = $('[data-toggle="collapse"]') + $colltabs.each(function( index ) { + var colltab = $(this) + , collpanel = (colltab.attr('data-target')) ? $(colltab.attr('data-target')) : $(colltab.attr('href')) + , parent = colltab.attr('data-parent') + , collparent = parent && $(parent) + , collid = colltab.attr('id') || uniqueId('ui-collapse') + + colltab.attr('id', collid) + + if(collparent){ + colltab.attr({ 'role':'tab', 'aria-selected':'false', 'aria-expanded':'false' }) + $(collparent).find('div:not(.collapse,.panel-body), h4').attr('role','presentation') + collparent.attr({ 'role' : 'tablist', 'aria-multiselectable' : 'true' }) + + if(collpanel.hasClass('in')){ + colltab.attr({ 'aria-controls': collpanel.attr('id'), 'aria-selected':'true', 'aria-expanded':'true', 'tabindex':'0' }) + collpanel.attr({ 'role':'tabpanel', 'tabindex':'0', 'aria-labelledby':collid, 'aria-hidden':'false' }) + }else{ + colltab.attr({'aria-controls' : collpanel.attr('id'), 'tabindex':'-1' }) + collpanel.attr({ 'role':'tabpanel', 'tabindex':'-1', 'aria-labelledby':collid, 'aria-hidden':'true' }) + } + } + }) + + var collToggle = $.fn.collapse.Constructor.prototype.toggle + $.fn.collapse.Constructor.prototype.toggle = function(){ + var prevTab = this.$parent && this.$parent.find('[aria-expanded="true"]') , href + + if(prevTab){ + var prevPanel = prevTab.attr('data-target') || (href = prevTab.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') + , $prevPanel = $(prevPanel) + , $curPanel = this.$element + , par = this.$parent + , curTab + + if (this.$parent) curTab = this.$parent.find('[data-toggle=collapse][href="#' + this.$element.attr('id') + '"]') + + collToggle.apply(this, arguments) + + if ($.support.transition) { + this.$element.one($.support.transition.end, function(){ + + prevTab.attr({ 'aria-selected':'false','aria-expanded':'false', 'tabIndex':'-1' }) + $prevPanel.attr({ 'aria-hidden' : 'true','tabIndex' : '-1'}) + + curTab.attr({ 'aria-selected':'true','aria-expanded':'true', 'tabIndex':'0' }) + + if($curPanel.hasClass('in')){ + $curPanel.attr({ 'aria-hidden' : 'false','tabIndex' : '0' }) + }else{ + curTab.attr({ 'aria-selected':'false','aria-expanded':'false'}) + $curPanel.attr({ 'aria-hidden' : 'true','tabIndex' : '-1' }) + } + }) + } + }else{ + collToggle.apply(this, arguments) + } + } + + $.fn.collapse.Constructor.prototype.keydown = function (e) { + var $this = $(this) + , $items + , $tablist = $this.closest('div[role=tablist] ') + , index + , k = e.which || e.keyCode + + $this = $(this) + if (!/(32|37|38|39|40)/.test(k)) return + if(k==32) $this.click() + + $items = $tablist.find('[role=tab]') + index = $items.index($items.filter(':focus')) + + if (k == 38 || k == 37) index-- // up & left + if (k == 39 || k == 40) index++ // down & right + if(index < 0) index = $items.length -1 + if(index == $items.length) index = 0 + + $items.eq(index).focus() + + e.preventDefault() + e.stopPropagation() + + } + + $(document).on('keydown.collapse.data-api','[data-toggle="collapse"]' , $.fn.collapse.Constructor.prototype.keydown); + + +// Carousel Extension + // =============================== + + $('.carousel').each(function (index) { + + // This function positions a highlight box around the tabs in the tablist to use in focus styling + + function setTablistHighlightBox() { + + var $tab + , offset + , height + , width + , highlightBox = {} + + highlightBox.top = 0 + highlightBox.left = 32000 + highlightBox.height = 0 + highlightBox.width = 0 + + for (var i = 0; i < $tabs.length; i++) { + $tab = $tabs[i] + offset = $($tab).offset() + height = $($tab).height() + width = $($tab).width() + +// console.log(" Top: " + offset.top + " Left: " + offset.left + " Height: " + height + " Width: " + width) + + if (highlightBox.top < offset.top) { + highlightBox.top = Math.round(offset.top) + } + + if (highlightBox.height < height) { + highlightBox.height = Math.round(height) + } + + if (highlightBox.left > offset.left) { + highlightBox.left = Math.round(offset.left) + } + + var w = (offset.left - highlightBox.left) + Math.round(width) + + if (highlightBox.width < w) { + highlightBox.width = w + } + + } // end for + +// console.log("[HIGHLIGHT] Top: " + highlightBox.top + " Left: " + highlightBox.left + " Height: " + highlightBox.height + " Width: " + highlightBox.width) + + $tablistHighlight.style.top = (highlightBox.top - 2) + 'px' + $tablistHighlight.style.left = (highlightBox.left - 2) + 'px' + $tablistHighlight.style.height = (highlightBox.height + 7) + 'px' + $tablistHighlight.style.width = (highlightBox.width + 8) + 'px' + + } // end function + + var $this = $(this) + , $prev = $this.find('[data-slide="prev"]') + , $next = $this.find('[data-slide="next"]') + , $tablist = $this.find('.carousel-indicators') + , $tabs = $this.find('.carousel-indicators li') + , $tabpanels = $this.find('.item') + , $tabpanel + , $tablistHighlight + , $pauseCarousel + , $complementaryLandmark + , $tab + , $is_paused = false + , offset + , height + , width + , i + , id_title = 'id_title' + , id_desc = 'id_desc' + + + $tablist.attr('role', 'tablist') + + $tabs.focus(function() { + $this.carousel('pause') + $is_paused = true + $pauseCarousel.innerHTML = "Play Carousel" + $(this).parent().addClass('active'); +// $(this).addClass('focus') + setTablistHighlightBox() + $($tablistHighlight).addClass('focus') + $(this).parents('.carousel').addClass('contrast') + }) + + $tabs.blur(function(event) { + $(this).parent().removeClass('active'); +// $(this).removeClass('focus') + $($tablistHighlight).removeClass('focus') + $(this).parents('.carousel').removeClass('contrast') + }) + + + for (i = 0; i < $tabpanels.length; i++) { + $tabpanel = $tabpanels[i] + $tabpanel.setAttribute('role', 'tabpanel') + $tabpanel.setAttribute('id', 'tabpanel-' + index + '-' + i) + $tabpanel.setAttribute('aria-labelledby', 'tab-' + index + '-' + i) + } + + if (typeof $this.attr('role') !== 'string') { + $this.attr('role', 'complementary'); + $this.attr('aria-labelledby', id_title); + $this.attr('aria-describedby', id_desc); + $this.prepend('

          A carousel is a rotating set of images, rotation stops on keyboard focus on carousel tab controls or hovering the mouse pointer over images. Use the tabs or the previous and next buttons to change the displayed slide.

          ') + $this.prepend('

          Carousel content with ' + $tabpanels.length + ' slides.

          ') + } + + + for (i = 0; i < $tabs.length; i++) { + $tab = $tabs[i] + + $tab.setAttribute('role', 'tab') + $tab.setAttribute('id', 'tab-' + index + '-' + i) + $tab.setAttribute('aria-controls', 'tabpanel-' + index + '-' + i) + + var tpId = '#tabpanel-' + index + '-' + i + var caption = $this.find(tpId).find('h1').text() + + if ((typeof caption !== 'string') || (caption.length === 0)) caption = $this.find(tpId).text() + if ((typeof caption !== 'string') || (caption.length === 0)) caption = $this.find(tpId).find('h3').text() + if ((typeof caption !== 'string') || (caption.length === 0)) caption = $this.find(tpId).find('h4').text() + if ((typeof caption !== 'string') || (caption.length === 0)) caption = $this.find(tpId).find('h5').text() + if ((typeof caption !== 'string') || (caption.length === 0)) caption = $this.find(tpId).find('h6').text() + if ((typeof caption !== 'string') || (caption.length === 0)) caption = "no title"; + +// console.log("CAPTION: " + caption ) + + var tabName = document.createElement('span') + tabName.setAttribute('class', 'sr-only') + tabName.innerHTML='Slide ' + (i+1) + if (caption) tabName.innerHTML += ": " + caption + $tab.appendChild(tabName) + + } + + // create div for focus styling of tablist + $tablistHighlight = document.createElement('div') + $tablistHighlight.className = 'carousel-tablist-highlight' + document.body.appendChild($tablistHighlight) + + // create button for screen reader users to stop rotation of carousel + + // create button for screen reader users to pause carousel for virtual mode review + $complementaryLandmark = document.createElement('aside') + $complementaryLandmark.setAttribute('class', 'carousel-aside-pause') + $complementaryLandmark.setAttribute('aria-label', 'carousel pause/play control') + $this.prepend($complementaryLandmark) + + $pauseCarousel = document.createElement('button') + $pauseCarousel.className = "carousel-pause-button" + $pauseCarousel.innerHTML = "Pause Carousel" + $pauseCarousel.setAttribute('title', "Pause/Play carousel button can be used by screen reader users to stop carousel animations") + $($complementaryLandmark).append($pauseCarousel) + + $($pauseCarousel).click(function() { + if ($is_paused) { + $pauseCarousel.innerHTML = "Pause Carousel" + $this.carousel('cycle') + $is_paused = false + } + else { + $pauseCarousel.innerHTML = "Play Carousel" + $this.carousel('pause') + $is_paused = true + } + }) + $($pauseCarousel).focus(function() { + $(this).addClass('focus') + }) + + $($pauseCarousel).blur(function() { + $(this).removeClass('focus') + }) + + setTablistHighlightBox() + + $( window ).resize(function() { + setTablistHighlightBox() + }) + + // Add space bar behavior to prev and next buttons for SR compatibility + $prev.attr('aria-label', 'Previous Slide') + $prev.keydown(function(e) { + var k = e.which || e.keyCode + if (/(13|32)/.test(k)) { + e.preventDefault() + e.stopPropagation() + $prev.trigger('click'); + } + }); + + $prev.focus(function() { + $(this).parents('.carousel').addClass('contrast') + }) + + $prev.blur(function() { + $(this).parents('.carousel').removeClass('contrast') + }) + + $next.attr('aria-label', 'Next Slide') + $next.keydown(function(e) { + var k = e.which || e.keyCode + if (/(13|32)/.test(k)) { + e.preventDefault() + e.stopPropagation() + $next.trigger('click'); + } + }); + + $next.focus(function() { + $(this).parents('.carousel').addClass('contrast') + }) + + $next.blur(function() { + $(this).parents('.carousel').removeClass('contrast') + }) + + $('.carousel-inner a').focus(function() { + $(this).parents('.carousel').addClass('contrast') + }) + + $('.carousel-inner a').blur(function() { + $(this).parents('.carousel').removeClass('contrast') + }) + + $tabs.each(function () { + var item = $(this) + if(item.hasClass('active')) { + item.attr({ 'aria-selected': 'true', 'tabindex' : '0' }) + }else{ + item.attr({ 'aria-selected': 'false', 'tabindex' : '-1' }) + } + }) + }) + + var slideCarousel = $.fn.carousel.Constructor.prototype.slide + $.fn.carousel.Constructor.prototype.slide = function (type, next) { + var $element = this.$element + , $active = $element.find('[role=tabpanel].active') + , $next = next || $active[type]() + , $tab + , $tab_count = $element.find('[role=tabpanel]').size() + , $prev_side = $element.find('[data-slide="prev"]') + , $next_side = $element.find('[data-slide="next"]') + , $index = 0 + , $prev_index = $tab_count -1 + , $next_index = 1 + , $id + + if ($next && $next.attr('id')) { + $id = $next.attr('id') + $index = $id.lastIndexOf("-") + if ($index >= 0) $index = parseInt($id.substring($index+1), 10) + + $prev_index = $index - 1 + if ($prev_index < 1) $prev_index = $tab_count - 1 + + $next_index = $index + 1 + if ($next_index >= $tab_count) $next_index = 0 + } + + $prev_side.attr('aria-label', 'Show slide ' + ($prev_index+1) + ' of ' + $tab_count) + $next_side.attr('aria-label', 'Show slide ' + ($next_index+1) + ' of ' + $tab_count) + + + slideCarousel.apply(this, arguments) + + $active + .one('bsTransitionEnd', function () { + var $tab + + $tab = $element.find('li[aria-controls="' + $active.attr('id') + '"]') + if ($tab) $tab.attr({'aria-selected':false, 'tabIndex': '-1'}) + + $tab = $element.find('li[aria-controls="' + $next.attr('id') + '"]') + if ($tab) $tab.attr({'aria-selected': true, 'tabIndex': '0'}) + + }) + } + + var $this; + $.fn.carousel.Constructor.prototype.keydown = function (e) { + + $this = $this || $(this) + if(this instanceof Node) $this = $(this) + + function selectTab(index) { + if (index >= $tabs.length) return + if (index < 0) return + + $carousel.carousel(index) + setTimeout(function () { + $tabs[index].focus() + // $this.prev().focus() + }, 150) + } + + var $carousel = $(e.target).closest('.carousel') + , $tabs = $carousel.find('[role=tab]') + , k = e.which || e.keyCode + , index + + if (!/(37|38|39|40)/.test(k)) return + + index = $tabs.index($tabs.filter('.active')) + if (k == 37 || k == 38) { // Up + index-- + selectTab(index); + } + + if (k == 39 || k == 40) { // Down + index++ + selectTab(index); + } + + e.preventDefault() + e.stopPropagation() + } + $(document).on('keydown.carousel.data-api', 'li[role=tab]', $.fn.carousel.Constructor.prototype.keydown); + + + })(jQuery); \ No newline at end of file diff --git a/public-new/app/mailers/.keep b/public-new/app/assets/javascripts/channels/.keep similarity index 100% rename from public-new/app/mailers/.keep rename to public-new/app/assets/javascripts/channels/.keep diff --git a/public-new/app/assets/javascripts/cite.js b/public-new/app/assets/javascripts/cite.js new file mode 100644 index 0000000000..a725e24c64 --- /dev/null +++ b/public-new/app/assets/javascripts/cite.js @@ -0,0 +1,29 @@ +function setupCite(modalId, text){ + setupClip(modalId, text, 'citeThis', 'cite'); + $('#cite_sub').submit(function(e) { + cite(); + return false; + }); +} + +function setupClip(modalId, btnText,target, type ) { + var $modal = $('#' + modalId); + $modal.find('div.modal-body').attr('id', target); + var x = $modal.find('.action-btn'); + var btn; + if (x.length == 1) { + btn = x[0]; + } + else { + btn = x; + } + $(btn).attr('id', type+ "_btn"); + $(btn).addClass('clip-btn'); + $(btn).attr('data-clipboard-target', '#'+target); + $(btn).html(btnText); + new Clipboard('.clip-btn'); +} + +function cite() { + $("#cite_modal").modal('show'); +} diff --git a/public-new/app/assets/javascripts/classifications.js b/public-new/app/assets/javascripts/classifications.js deleted file mode 100644 index 1932599275..0000000000 --- a/public-new/app/assets/javascripts/classifications.js +++ /dev/null @@ -1,47 +0,0 @@ -var app = app || {}; -(function(Bb, _, $) { - - - var ClassificationSidebarView = Bb.View.extend({ - el: "#sidebar-container", - - initialize: function(nodeUri) { - this.url = "/api"+nodeUri+"/tree"; - this.render(); - }, - - render: function() { - var $el = this.$el; - - $el.addClass('classification-tree'); - - $.ajax(this.url, { - success: function(data) { - app.debug.tree = data; - - //TODO - make once - var displayString = function(container_child) { - var result = container_child.container_1; - result += _.has(container_child, 'container_2') ? container_child.container_2 : ''; - return result; - }; - - var containerUri = function (container_child) { - var result = container_child.resource_data.repository + "/" + _.pluralize(app.utils.getPublicType(container_child.resource_data.type)) + "/" + container_child.resource_data.id; - - return result; - }; - - - $el.html(app.utils.tmpl('classification-tree', {classifications: data, displayString: displayString, containerUri: containerUri, title: "Subgroups of the Record Group"})); - - $(".classification-tree").foundation(); - } - }); - } - - }); - - app.ClassificationSidebarView = ClassificationSidebarView; - -})(Backbone, _, jQuery); diff --git a/public-new/app/assets/javascripts/embedded-search.js b/public-new/app/assets/javascripts/embedded-search.js deleted file mode 100644 index 76565f7fa6..0000000000 --- a/public-new/app/assets/javascripts/embedded-search.js +++ /dev/null @@ -1,100 +0,0 @@ -var app = app || {}; - -(function(Bb, _) { - - function getUrlForPage(page) { - var current = window.location.pathname; - var result = ""; - if(current.match(/page=\d+/)) { - result = current.replace(/page=\d+/, "page="+page); - } else if(current.match(/\?/)) { - result = current + "page="+page; - } else { - result = current +"?page="+page; - } - return result; - } - - var EmbeddedSearchView = Bb.View.extend({ - el: "#embedded-search-container", - initialize: function(opts) { - var data = {}; - data.title = opts.title || "Related Collections"; - - this.$el.html(app.utils.tmpl('embedded-search', data)); - var $editorContainer = $("#search-editor-container", this.$el); - this.query = new app.SearchQuery(); - this.query.advanced = true; - this.searchEditor = new app.SearchEditor($editorContainer); - this.searchEditor.addRow(); - this.searchResults = new app.SearchResults([], { - state: _.merge({ - pageSize: 10 - }, opts) - }); - app.debug.searchResults = this.searchResults; - this.searchResults.advanced = true; //TODO - make advanced default - this.searchResultsView = new app.SearchResultsView({ - collection: this.searchResults, - query: this.query, - baseUrl: opts.baseUrl - }); - - var searchResults = this.searchResults; - var searchResultsView = this.searchResultsView; - var destroy = $.proxy(this.destroy, this); - - this.searchResultsView.on("changepage.aspace", function(page) { - $('#wait-modal').foundation('open'); - searchResults.changePage(page).then(function() { - var url = getUrlForPage(page); - app.router.navigate(url); - searchResultsView.render(); - setTimeout(function() { - $('#wait-modal').foundation('close'); - // reinitalize foundation - $("#main-content").foundation(); - }, 500); - }); - }); - - this.searchResultsView.on("showrecord.aspace", function(publicUrl) { - destroy(); - app.router.showRecord(publicUrl); - }); - - $editorContainer.addClass("search-panel-blue"); - - this.update(false); - - }, - - events: { - "click #search-button" : function(e) { - e.preventDefault(); - this.query.updateCriteria(this.searchEditor.extract()); - - this.update(true); - } - - }, - - update: function(rewind) { - var searchResultsView = this.searchResultsView; - this.searchResults.updateQuery(this.query.toArray(), rewind).then(function() { - searchResultsView.render(); - }); - }, - - destroy: function() { - this.unbind(); - this.$el.empty(); - } - - - }); - - - app.EmbeddedSearchView = EmbeddedSearchView; - -})(Backbone, _); diff --git a/public-new/app/assets/javascripts/handle_accordion.js b/public-new/app/assets/javascripts/handle_accordion.js new file mode 100644 index 0000000000..f56fc1dc37 --- /dev/null +++ b/public-new/app/assets/javascripts/handle_accordion.js @@ -0,0 +1,28 @@ +/* handle the accordions */ +/* we need to pass in the text that is obtained via the config/locales/*yml file */ +var expand_text = ""; +var collapse_text = ""; +/* we don't provide a button if there's only one panel; neither do we collapse that one panel */ +function initialize_accordion(what, ex_text, col_text) { + expand_text = ex_text; + collapse_text = col_text; + if ($(what).size() > 1 && $(what).parents(".acc_holder").size() ===1 ) { + if ($(what).parents(".acc_holder").children(".acc_button").size() == 0) { + $(what).parents(".acc_holder").prepend(""); + } + collapse_all(what, false); + } +} +function collapse_all(what, expand) { + $(what).each(function() { $(this).collapse(((expand)? "show" : "hide")); } ); + set_button(what, !expand); +} + +function set_button(what, expand) { + $holder = $(what).parents(".acc_holder"); + $btn = $holder.children('.acc_button'); + if ($btn.size() === 1) { + $btn.text((expand)?expand_text : collapse_text); + $btn.attr("href", "javascript:collapse_all('" + what + "'," + expand +")"); + } +} \ No newline at end of file diff --git a/public-new/app/assets/javascripts/icons.js b/public-new/app/assets/javascripts/icons.js deleted file mode 100644 index 07aba1230b..0000000000 --- a/public-new/app/assets/javascripts/icons.js +++ /dev/null @@ -1,25 +0,0 @@ -var app = app || {}; - -(function(Bb, _) { - 'use strict'; - - var typeToIconMap = { - resource: "glyphicon glyphicon-list-alt", - archival_object: "fi-archive", - accession: "glyphicon glyphicon-list-alt", - classification: "fi-page-multiple", - classification_term: "fi-page-multiple", - subject: "glyphicon glyphicon-tags", - agent_person: "fi-torso", - agent_corporation: "glyphicon glyphicon-briefcase", - agent_family: "torsos-male-female", - repository: "fi-home" - }; - - app.icons = { - getIconClass: function(recordType) { - return typeToIconMap[recordType]; - } - }; - -})(Backbone, _); diff --git a/public-new/app/assets/javascripts/index.coffee b/public-new/app/assets/javascripts/index.coffee new file mode 100644 index 0000000000..24f83d18bb --- /dev/null +++ b/public-new/app/assets/javascripts/index.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/public-new/app/assets/javascripts/infinite_scroll.js b/public-new/app/assets/javascripts/infinite_scroll.js new file mode 100644 index 0000000000..4f6272fbc4 --- /dev/null +++ b/public-new/app/assets/javascripts/infinite_scroll.js @@ -0,0 +1,340 @@ +(function(exports) { + + var BATCH_SIZE = 2; + var SCROLL_DELAY_MS = 50; + var SCROLL_DRAG_DELAY_MS = 500; + var LOAD_THRESHOLD_PX = 5000; + + function InfiniteScroll(base_url, elt, recordCount, loaded_callback) { + this.base_url = base_url; + this.wrapper = elt; + this.elt = elt.find('.infinite-record-container'); + this.recordCount = recordCount; + + this.scrollPosition = 0; + this.scrollbarElt = undefined; + + this.scrollCallbacks = []; + + this.initScrollbar(); + this.initEventHandlers(); + this.considerPopulatingWaypoints(false, null, loaded_callback); + + this.globalStyles = $(' + + + + <%= yield %> + + diff --git a/public-new/app/views/layouts/mailer.text.erb b/public-new/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/public-new/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/public-new/app/views/objects/request_showing.html.erb b/public-new/app/views/objects/request_showing.html.erb new file mode 100644 index 0000000000..969490af6b --- /dev/null +++ b/public-new/app/views/objects/request_showing.html.erb @@ -0,0 +1,3 @@ + +<%= t('request.visit', :repo_name => @request[:repo_name], :archival_object => @request[:title]).html_safe %> + <%= render partial: 'shared/request_form' %> diff --git a/public-new/app/views/objects/show.html.erb b/public-new/app/views/objects/show.html.erb new file mode 100644 index 0000000000..d72a293b57 --- /dev/null +++ b/public-new/app/views/objects/show.html.erb @@ -0,0 +1,31 @@ + +
          +
          +
          + <%= render partial: 'shared/idbadge', locals: {:result => @result, :props => { :full => true} } %> +
          +
          + <%= render partial: 'shared/page_actions', locals: {:record => @result, :title => @result.display_string, :url => request.fullpath, :cite => @result.cite } %> +
          +
          +
          + <%= render partial: 'shared/breadcrumbs' %> +
          + +
          +
          + <% if defined?(comp_id) && !comp_id && !@result['json']['ref_id'].blank? %> + [<%= t('archival_object._public.header.ref_id') %>: <%= @result['json']['ref_id'] %>] + <% end %> + <%= render partial: 'shared/digital', locals: {:dig_objs => @dig} %> + + <%= render partial: 'shared/record_innards' %> +
          + + +
          + <%= render partial: 'shared/modal_actions' %> +
          + diff --git a/public-new/app/views/repositories/_badge.html.erb b/public-new/app/views/repositories/_badge.html.erb new file mode 100644 index 0000000000..2f5b8944b7 --- /dev/null +++ b/public-new/app/views/repositories/_badge.html.erb @@ -0,0 +1,30 @@ +<%# the "badges" for resources, records, etc. Expects one local: type[ of badge]. @sublist_action, @counts, and @result carry through %> + +
        • +
          + + +
          +
        • diff --git a/public-new/app/views/repositories/_full_repo.html.erb b/public-new/app/views/repositories/_full_repo.html.erb new file mode 100644 index 0000000000..7edcb638cc --- /dev/null +++ b/public-new/app/views/repositories/_full_repo.html.erb @@ -0,0 +1,51 @@ +<% if url && url!= "http://url.unspecified" %> + +<% end %> + +<% unless info.blank? %> +<% lead = t("repos.#{info['top']['repo_code'].downcase}.lead_graph", default: '') %> +<% unless lead.blank? %> +
          <%= lead.html_safe %>
          +<% end %> + +
          + <%= t('contact') %>:
          +<% if info["address"].present? %> + + <%= info["address"].join("
          ").html_safe %> +
          +
          +<% end %> + +<% %w{city region post_code country}.each do |type| + if info[type].present? %> + <%= info[type] %> + <% end %> +<% end %> + +
          + +<% if info["telephones"].present? %> + <% info["telephones"].each do |phone| %> + + <% if !phone['number'].blank? %> + <% if !phone['number_type'].blank? && phone['number_type'].strip.upcase == 'FAX' %> + <%= phone['number'] %> (<%= t('fax') %>) + <% else %> + <%= phone['number'] %> + <% end %> + <% end %> +
          + <% end %> +<% end %> + + +<% if info["email"].present? %> + +
          +<% end %> +
          +<% end %> + diff --git a/public-new/app/views/repositories/_repository.html.erb b/public-new/app/views/repositories/_repository.html.erb new file mode 100644 index 0000000000..49e6464f94 --- /dev/null +++ b/public-new/app/views/repositories/_repository.html.erb @@ -0,0 +1,33 @@ +<% if !full %> +
          +<% end %> + +<%= (full ? '

          ' : '

          ').html_safe %> +<% if !full %><% end %><%= result['name']%> +<% if !full %><% end %> + <% if result.has_key?('image_url') %> + + <% end %> +<%= (full ? '

          ' : '').html_safe %> + +
          +
          +  <%= t('repository._singular') %> +
          + +
          + <% if result['parent_institution_name'].present? %> +
          <%= t('parent_inst') %>: + <%= result['parent_institution_name'] %> +
          + <% end %> + <% if full %> + <%= render partial: 'repositories/full_repo', locals: {:info => result['repo_info'], :url => result['url']} %> + <% else %> +
          Number of <%= t('resource._plural') %>: <%= result['count'] %>
          + <% end %> +
          +
          +<% if !full %> +
          +<% end %> diff --git a/public-new/app/views/repositories/_repository_details.html.erb b/public-new/app/views/repositories/_repository_details.html.erb new file mode 100644 index 0000000000..0936123961 --- /dev/null +++ b/public-new/app/views/repositories/_repository_details.html.erb @@ -0,0 +1,4 @@ +

          <%= t('repository.details') %>

          +

          <%= "#{t('repository.part')} #{@repo_info['top']['name']} #{t('repository._singular')}" %>

          +<%= render partial: 'repositories/full_repo', locals: {:info => @repo_info, :url => @repo_info['top']['url']} %> + diff --git a/public-new/app/views/repositories/index.html.erb b/public-new/app/views/repositories/index.html.erb new file mode 100644 index 0000000000..0c825f6cb8 --- /dev/null +++ b/public-new/app/views/repositories/index.html.erb @@ -0,0 +1,12 @@ +
          +
          +

          <%= @search_data['total_hits'] %> <%= (@search_data['total_hits'] == 1 ? t('repository._singular'): t('repository._plural')) %>

          + +<%= render partial: 'shared/pagination', locals: {:pager => @pager} %> +<% @json.each do |result| %> +<%= render partial: 'repositories/repository', locals: {:result => result, :full => false} %> + +<% end %> +<%= render partial: 'shared/pagination', locals: {:pager => @pager} %> +
          +
          diff --git a/public-new/app/views/repositories/show.html.erb b/public-new/app/views/repositories/show.html.erb new file mode 100644 index 0000000000..874a22d81c --- /dev/null +++ b/public-new/app/views/repositories/show.html.erb @@ -0,0 +1,27 @@ +
          +
          + <%= render partial: 'repositories/repository', locals: {:result => @result, :full => true} %> +
          + +
          +

          <%= t('repository.what') %>

          +
          +
            + <% @badges.each do |type| %> + <%= render partial: 'repositories/badge', locals: {:type => type} %> + <% end %> +
          +
          + +
          + <%= render partial: 'shared/search', locals: {:search_url => "#{@result['uri']}/search", + :title => t('repository._singular'), + :limit_options => [["#{t('actions.search')} #{t('search-limits.all')}",''], + [t('search-limit', :limit => t('search-limits.resources')),'resource'], + [t('search-limit', :limit => t('search-limits.digital')),'digital_object']], + + :field_options => [["#{t('search_results.filter.fullrecord')}",''], + ["#{t('search_results.filter.title')}",'title'], + ["#{t('search_results.filter.creators')}",'creators_text'], + ["#{t('search_results.filter.notes')}", 'notes'] ] } %> +
          diff --git a/public-new/app/views/request_mailer/request_received_email.html.erb b/public-new/app/views/request_mailer/request_received_email.html.erb new file mode 100644 index 0000000000..ddadcb6221 --- /dev/null +++ b/public-new/app/views/request_mailer/request_received_email.html.erb @@ -0,0 +1,19 @@ +

          Thank you for your request

          + +
          +

          Record Requested

          + Title: <%= @request.title %>
          + <% url = "#{AppConfig[:public_url].sub(/\/^/, '')}#{@request.request_uri}" %> + URL: <%= link_to url, url %> +
          + +
          +

          Your Details

          + Name: <%= @request.user_name %>
          + Email: <%= @request.user_email %>
          + Date: <%= @request.date %>
          + Notes for Staff: +
          + <%= (@request.note || "").gsub(/[\n\r]/,"
          \n").html_safe %> +
          +
          \ No newline at end of file diff --git a/public-new/app/views/request_mailer/request_received_email.txt.erb b/public-new/app/views/request_mailer/request_received_email.txt.erb new file mode 100644 index 0000000000..e0b79dca3a --- /dev/null +++ b/public-new/app/views/request_mailer/request_received_email.txt.erb @@ -0,0 +1,12 @@ +Thank you for your request. + +-- Record Requested -- +Title: <%= @request.title %> +URL: <%= "#{AppConfig[:public_url].sub(/\/^/, '')}#{@request.request_uri}" %> + +-- Your Details -- +Name: <%= @request.user_name %> +Email: @request.user_email %> +Date: <%= @request.date %> +Notes for Staff: +<%= @request.note %> \ No newline at end of file diff --git a/public-new/app/views/request_mailer/request_received_staff_email.html.erb b/public-new/app/views/request_mailer/request_received_staff_email.html.erb new file mode 100644 index 0000000000..00ad62d60c --- /dev/null +++ b/public-new/app/views/request_mailer/request_received_staff_email.html.erb @@ -0,0 +1,21 @@ +

          New Request

          +

          Request received: <%= Time.now %>

          + +
          +

          Requester

          + Name: <%= @request.user_name %>
          + Email: <%= @request.user_email %>
          + Date: <%= @request.date %>
          + Notes for Staff: +
          + <%= (@request.note || "").gsub(/[\n\r]/,"
          \n") %> +
          + +
          + +
          +

          Record Requested

          + <%= @request.to_text_array(false).each do |info| %> + <%= info%>
          + <% end %> +
          \ No newline at end of file diff --git a/public-new/app/views/resources/_finding_aid.html.erb b/public-new/app/views/resources/_finding_aid.html.erb new file mode 100644 index 0000000000..d89d695290 --- /dev/null +++ b/public-new/app/views/resources/_finding_aid.html.erb @@ -0,0 +1,22 @@ +<% order = %w(title subtitle status author date description_rules language sponsor edition_statement series statement) %> + +<% unless @result.finding_aid.blank? %> + <% if @result.finding_aid.size > 1 || @result.finding_aid['revisions'].blank? %> +
          + <% order.each do |item| %> + <% unless @result.finding_aid[item].blank? %> +
          <%= t("resource._public.finding_aid.#{item}") %>
          +
          <%= @result.finding_aid[item] %>
          + <% end %> + <% end %> +
          + <% end %> + <% unless @result.finding_aid['revision'].blank? %> +

          <%= t('resource._public.finding_aid.revision') %>

          +
            + <% @result.finding_aid['revision'].each do |rev| %> +
          • <%= rev['date']%>: <%= rev['desc'] %>
          • + <% end %> +
          + <% end %> +<% end %> diff --git a/public-new/app/views/resources/_infinite_item.html.erb b/public-new/app/views/resources/_infinite_item.html.erb new file mode 100644 index 0000000000..9277996883 --- /dev/null +++ b/public-new/app/views/resources/_infinite_item.html.erb @@ -0,0 +1,44 @@ +
          + <%= render partial: 'shared/idbadge', locals: {:result => @result, :props => { :full => false} } %> +
          + +<% scopecontent_note = @result.note('scopecontent') %> +<% if scopecontent_note %> + <%= render partial: 'shared/single_note', locals: {:type => 'abstract', :note_struct => scopecontent_note, :notitle => true} %> +<% end %> + +<% accessrestrict_note = @result.note('accessrestrict') %> +<% if accessrestrict_note %> + <%= render partial: 'shared/single_note', locals: {:type => 'accessrestrict', :note_struct => accessrestrict_note, :notitle => false} %> +<% end %> + +<% unless @result.dates.blank? %> +

          <%= t('resource._public.dates') %>

          + <%= render partial: 'shared/dates', locals: {:dates => @result.dates} %> +<% end %> + +<% unless @result.extents.blank? %> +

          <%= t('resource._public.extent') %>

          + <% @result.extents.each do |ext| %> +

          <%= inheritance(ext['_inherited']).html_safe %> + <%= ext['display']%> +

          + <% end %> +<% end %> + +<% unless @result.agents.blank? %> +

          <%= t('pui_agent.related') %>

          + <%= render partial: 'shared/present_list', locals: {:list => @result.agents, :list_clss => 'agents_list'} %> +<% end %> + +<% if @result['language'] %> +
          +
          <%= t('resource._public.finding_aid.language')%>
          +
          <%= @result['language'].upcase %>
          +
          +<% else %> + <% langmaterial_note = @result.note('langmaterial') %> + <% if langmaterial_note %> + <%= render partial: 'shared/single_note', locals: {:type => 'langmaterial', :note_struct => langmaterial_note, :notitle => false} %> + <% end %> +<% end %> \ No newline at end of file diff --git a/public-new/app/views/resources/_related_accessions.html.erb b/public-new/app/views/resources/_related_accessions.html.erb new file mode 100644 index 0000000000..0979241646 --- /dev/null +++ b/public-new/app/views/resources/_related_accessions.html.erb @@ -0,0 +1,20 @@ +

          <%= t('resource._public.related_accessions.accessions') %>

          +
            + <% accessions.each do |accession| %> +
          • + <% if accession.acquisition_type %> + <%= accession.acquisition_type %>: + <% end %> + <%= link_to accession.display_string, accession.uri %> +
          • + <% end %> +
          + +<% unless deaccessions.empty? %> +

          <%= t('resource._public.related_accessions.deaccessions') %>

          +
            + <% deaccessions.each do |deaccession| %> +
          • <%= deaccession['description'] %>
          • + <% end %> +
          +<% end %> \ No newline at end of file diff --git a/public-new/app/views/resources/index.html.erb b/public-new/app/views/resources/index.html.erb new file mode 100644 index 0000000000..18a92b906b --- /dev/null +++ b/public-new/app/views/resources/index.html.erb @@ -0,0 +1,10 @@ +
          +

          <%= (@repo_name != '' ? "#{@repo_name}: " : '') %><%= @results['total_hits'] %> <%= (@results['total_hits'] == 1 ? t('resource._singular'): t('resource._plural')) %>

          +
          +<%= render partial: 'shared/pagination', locals: {:pager => @pager} %> + <% @results['results'].each do |result| %> + <%= render partial: 'shared/result', locals: {:result => result, :props => {:full => false, :no_repo => !@repo_name.blank?}} %> + <% end %> +<%= render partial: 'shared/pagination', locals: {:pager => @pager} %> +
          +
          diff --git a/public-new/app/views/resources/infinite.html.erb b/public-new/app/views/resources/infinite.html.erb new file mode 100644 index 0000000000..997eb4568c --- /dev/null +++ b/public-new/app/views/resources/infinite.html.erb @@ -0,0 +1,91 @@ + + +
          +
          + <%= render partial: 'shared/idbadge', locals: {:result => @result, :props => { :full => true} } %> +
          +
          + <%= render partial: 'shared/page_actions', locals: {:record => @result, :title => @result.display_string, :url => request.fullpath, :cite => @result.cite } %> +
          +
          + +
          + <%= render partial: 'shared/breadcrumbs' %> +
          + +
          + +
          + +<%= javascript_include_tag 'treesync' %> +<%= javascript_include_tag 'infinite_scroll' %> +<%= javascript_include_tag 'largetree' %> +<%= javascript_include_tag 'tree_renderer' %> + +
          +
          +
          +
          + <% waypoint_size = 20 %> + <% @ordered_records.each_slice(waypoint_size).each_with_index do |refs, i| %> +
          data-uris="<%= refs.map {|r| r['ref']}.join(';') %>"> 
          + <% end %> +
          +
          +
          + +
          +
          +
          + + + + \ No newline at end of file diff --git a/public-new/app/views/resources/inventory.html.erb b/public-new/app/views/resources/inventory.html.erb new file mode 100644 index 0000000000..7228f70480 --- /dev/null +++ b/public-new/app/views/resources/inventory.html.erb @@ -0,0 +1,42 @@ + + +
          +
          + <%= render partial: 'shared/idbadge', locals: {:result => @result, :props => { :full => true} } %> +
          +
          + <%= render partial: 'shared/page_actions', locals: {:record => @result, :title => @result.display_string, :url => request.fullpath, :cite => @result.cite } %> +
          +
          + +
          + <%= render partial: 'shared/breadcrumbs' %> +
          + +
          + +
          + +
          +
          + <% unless @results.blank? || @results['total_hits'] == 0 %> + <% @results.records.each do |result| %> + <%= render partial: 'shared/result', locals: {:result => result, :props => {:full => false}} %> + <% end %> + <%= render partial: 'shared/pagination', locals: {:pager => @pager} %> + <% end %> +
          + +
          + +<%= render partial: 'shared/modal_actions' %> diff --git a/public-new/app/views/resources/show.html.erb b/public-new/app/views/resources/show.html.erb new file mode 100644 index 0000000000..f34a7ab1ad --- /dev/null +++ b/public-new/app/views/resources/show.html.erb @@ -0,0 +1,68 @@ + +
          +
          + <% unless defined?(@no_statement) || !defined?(@search) %> +
          <%= @search[:search_statement].html_safe %>
          + <% end %> +
          +
          + <%= render partial: 'shared/idbadge', locals: {:result => @result, :props => { :full => true} } %> +
          +
          + <%= render partial: 'shared/page_actions', locals: {:record => @result, :title => @result.display_string, :url => request.fullpath, :cite => @result.cite } %> +
          +
          +
          + <%= render partial: 'shared/breadcrumbs' %> +
          +
          + +
          +
          + +
          + <%= render partial: 'shared/digital', locals: {:dig_objs => @dig} %> + <%= render partial: 'shared/record_innards' %> +
          + +
          + +<%= render partial: 'shared/modal_actions' %> \ No newline at end of file diff --git a/public-new/app/views/search/search_results.html.erb b/public-new/app/views/search/search_results.html.erb new file mode 100644 index 0000000000..845cd76934 --- /dev/null +++ b/public-new/app/views/search/search_results.html.erb @@ -0,0 +1,41 @@ +<% results_type = (defined?(@results_type) ? @results_type : t('search_results.results')) %> +
          + <%= render partial: 'shared/breadcrumbs' %> + <% if defined?(@search_title) %> +

          <%= @search_title %>

          + <% end %> + + <% unless defined?(@no_statement) %> +
          <%= @search[:search_statement].html_safe %>
          + <% end %> + +

          <%= t('search_results.results_head', {:type => results_type, :start => @results['offset_first'], + :end => @results['offset_last'], :total => @results['total_hits'] }) %>

          +
          + + +
          +
          + + <%= render partial: 'shared/facets' %> +
          +
          + +
          + <%= render partial: 'shared/pagination', locals: {:pager => @pager} %> +
          + <%= render partial: 'shared/sorter' %> + +
          +
          + + <% @results.records.each do |result| %> + <%= render partial: 'shared/result', locals: {:result => result, :props => {:full => false}} %> + <% end %> +
          +
          + <%= render partial: 'shared/pagination', locals: {:pager => @pager} %> +
          +
          + +
          diff --git a/public-new/app/views/shared/_accordion_panel.html.erb b/public-new/app/views/shared/_accordion_panel.html.erb new file mode 100644 index 0000000000..716e9a0b28 --- /dev/null +++ b/public-new/app/views/shared/_accordion_panel.html.erb @@ -0,0 +1,16 @@ + +
          + +
          +
          + <%= p_body %> +
          +
          +
          + diff --git a/public-new/app/views/shared/_breadcrumbs.html.erb b/public-new/app/views/shared/_breadcrumbs.html.erb new file mode 100644 index 0000000000..3845185b95 --- /dev/null +++ b/public-new/app/views/shared/_breadcrumbs.html.erb @@ -0,0 +1,16 @@ +<% if defined?(@context) && !@context.blank? %> +
          + +
          +<% end %> diff --git a/public-new/app/views/shared/_children_tree.html.erb b/public-new/app/views/shared/_children_tree.html.erb new file mode 100644 index 0000000000..46a26b28de --- /dev/null +++ b/public-new/app/views/shared/_children_tree.html.erb @@ -0,0 +1,25 @@ +<%= javascript_include_tag 'largetree' %> +<%= javascript_include_tag 'tree_renderer' %> + +

          <%= heading_text %>

          +
          + + \ No newline at end of file diff --git a/public-new/app/views/shared/_cite_page_action.html.erb b/public-new/app/views/shared/_cite_page_action.html.erb new file mode 100644 index 0000000000..f2f1eda687 --- /dev/null +++ b/public-new/app/views/shared/_cite_page_action.html.erb @@ -0,0 +1,8 @@ +<%= form_tag "#{AppConfig[:public_prefix]}cite", :id => 'cite_sub' do |f| %> + <%= hidden_field_tag 'uri', record.uri %> + <%= hidden_field_tag 'cite', record.cite %> + +<% end %> \ No newline at end of file diff --git a/public-new/app/views/shared/_dates.html.erb b/public-new/app/views/shared/_dates.html.erb new file mode 100644 index 0000000000..51359592c1 --- /dev/null +++ b/public-new/app/views/shared/_dates.html.erb @@ -0,0 +1,8 @@ +
            + <% dates.each do |date| %> +
          • <%= inheritance(date['_inherited']).html_safe %> + <%= date['final_expression'] %> +
          • + <% end %> +
          + diff --git a/public-new/app/views/shared/_digital.html.erb b/public-new/app/views/shared/_digital.html.erb new file mode 100644 index 0000000000..8d50930a10 --- /dev/null +++ b/public-new/app/views/shared/_digital.html.erb @@ -0,0 +1,38 @@ +<%# expects 'dig_objs' as an array of hashes %> + +<% unless dig_objs.blank? %> +
          + <% dig_objs.each do |d_file| %> + <% if !d_file['out'].blank? %> + <% if d_file['thumb'].blank? %> +
          +
          + +
          +
          + + <% else %> +
          +
          + + <%= strip_mixed_content(d_file['caption'] || t('enumerations.instance_instance_type.digital_object')) %> + +
          + <%= (d_file['caption'] || t('enumerations.instance_instance_type.digital_object')).html_safe %> +
          +
          +
          + <% end %> + <% elsif !d_file['thumb'].blank? %> +
          + <%=  strip_mixed_content(d_file['caption'] || t('digital_object._public.thumbnail')) %> +
          + <% end %> + <% end %> +
          +<% end %> + diff --git a/public-new/app/views/shared/_display_notes.html.erb b/public-new/app/views/shared/_display_notes.html.erb new file mode 100644 index 0000000000..30ab8aac67 --- /dev/null +++ b/public-new/app/views/shared/_display_notes.html.erb @@ -0,0 +1,4 @@ + <% @result.notes.keys.each do |type| + note_struct = @result.note(type) %> + <%= render partial: 'shared/single_note', locals: {:type => type, :note_struct => note_struct} %> + <% end %> diff --git a/public-new/app/views/shared/_facets.html.erb b/public-new/app/views/shared/_facets.html.erb new file mode 100644 index 0000000000..6e63030732 --- /dev/null +++ b/public-new/app/views/shared/_facets.html.erb @@ -0,0 +1,65 @@ +<%# displaying chosen facet(s) (if any) and facets %> + +
          + <% unless @filters.blank? && @search.filters_blank? %> +

          <%= t('search_results.filtered_by')%>

          +
            + <% @search.get_filter_q_arr(@page_search).each do |f_q| %> + <% unless f_q['v'].blank? %> +
          • <%= t('search_results.search_term') %>: <%= f_q['v'] %> + X
          • + <% end %> + <% end %> + <% unless @search[:filter_from_year].blank? && @search[:filter_to_year].blank? %> + <% + from_year = (@search[:filter_from_year].blank? ? '' : @search[:filter_from_year] ) + to_year = (@search[:filter_to_year].blank? ? t('search_results.filter.year_now') : @search[:filter_to_year]) + %> +
          • <%= t('search_results.filter.from_to', {:begin=> from_year, :end => to_year })%> + " + title="<%= t('search_results.remove_filter') %> " class="delete_filter">X +
          • +
          + <% end %> + <% @filters.each do |k,h| %> +
        • <%= h['pt'] %>: <%= h['pv'] %> +X
        • + <% end %> +
        + <% end %> +
      + +<% if @search[:dates_within] || @search[:text_within] %> +

      <%= t('search_results.filter.head') %>

      +
      + <%= form_tag("#{@base_search}", method: 'get', :class=> "form-inline") do %> + <%= render partial: 'shared/hidden_params' %> + <% if @search[:text_within] %> +
      + <%= hidden_field_tag('sort', "") %> + <%= label_tag(:filter_q1, t('actions.search_within'), :class => 'sr-only') %> + <%= text_field_tag('filter_q[]', nil,:id => 'filter_q1', :placeholder => t('actions.search_within'), :class=> "form-control") %> +
      + <% end %> + <% if @search.search_dates_within? %> +
      + <%= label_tag(:filter_from_year, "#{t('search_results.filter.from_year')}", :class => 'sr-only') %> + <%= text_field_tag(:filter_from_year, nil, :size => 4,:maxlength => 4, :placeholder => t('search_results.filter.from'), + :class=>"form-control") %> + -- + <%= label_tag(:filter_to_year, "#{t('search_results.filter.to_year')}", :class=> 'sr-only') %> + <%= text_field_tag(:filter_to_year, nil, :size => 4, :maxlength => 4, :class=> "form-control", + :placeholder => t('search_results.filter.to')) %> +
      + <% else %> + <%= hidden_field_tag(:filter_from_year, @search[:filter_from_year]) %> + <%= hidden_field_tag(:filter_to_year, @search[:filter_to_year]) %> + <% end %> + + <%= submit_tag(t('search-button.label'), :class=>'btn btn-primary') %> + <% end %> +
      + +<% end %> +<%= render partial: 'shared/only_facets' %> diff --git a/public-new/app/views/shared/_footer.html.erb b/public-new/app/views/shared/_footer.html.erb new file mode 100644 index 0000000000..a875cc8fee --- /dev/null +++ b/public-new/app/views/shared/_footer.html.erb @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/public-new/app/views/shared/_header.html.erb b/public-new/app/views/shared/_header.html.erb new file mode 100644 index 0000000000..f0c57e6800 --- /dev/null +++ b/public-new/app/views/shared/_header.html.erb @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/public-new/app/views/shared/_hidden_params.html.erb b/public-new/app/views/shared/_hidden_params.html.erb new file mode 100644 index 0000000000..7076c9621d --- /dev/null +++ b/public-new/app/views/shared/_hidden_params.html.erb @@ -0,0 +1,11 @@ +<% params.each do |p, v| %> + <% if !%w{commit utf8 controller page sort filter_from_date filter_to_date}.include?(p) %> + <% if v.is_a? Array %> + <% v.each_with_index do |val,i| %> + <%= hidden_field_tag("#{p}[]".to_sym, val, :id => "#{p}_#{i}") %> + <% end %> + <% else %> + <%= hidden_field_tag(p.to_sym, v) %> + <% end %> + <% end %> + <% end %> diff --git a/public-new/app/views/shared/_idbadge.html.erb b/public-new/app/views/shared/_idbadge.html.erb new file mode 100644 index 0000000000..7b8a48e3e6 --- /dev/null +++ b/public-new/app/views/shared/_idbadge.html.erb @@ -0,0 +1,51 @@ +<% badge_label = t("#{result.primary_type}._singular") %> +<%= (props.fetch(:full,false)? '

      ' : '

      ').html_safe %> + <% if !props.fetch(:full,false) %> + <%= result.display_string %> + <% else %> + <%= result.display_string %> + <% end %> + <% unless result.container_display.blank? + if result.container_display.size == 1 + badge_label = result.container_display[0] + else + badge_label = t('multiple_containers') + end + end %> +<%= (props.fetch(:full,false)? '

      ' : '').html_safe %> + + +
      +
      +  <%= badge_label %> +
      + <% comp_id = result.identifier + if comp_id %> +
      + <%= t('search_sorting.identifier') %>: <%= comp_id %> +
      + <% end %> +
      diff --git a/public-new/app/views/shared/_modal.html.erb b/public-new/app/views/shared/_modal.html.erb new file mode 100644 index 0000000000..40fda68014 --- /dev/null +++ b/public-new/app/views/shared/_modal.html.erb @@ -0,0 +1,18 @@ +<%# handles modals. Requires: modal_id, title, modal_body, button_text %> + diff --git a/public-new/app/views/shared/_modal_actions.html.erb b/public-new/app/views/shared/_modal_actions.html.erb new file mode 100644 index 0000000000..c4d13c81a4 --- /dev/null +++ b/public-new/app/views/shared/_modal_actions.html.erb @@ -0,0 +1,14 @@ +<%# using clipboard.js: https://clipboardjs.com/ %> +<%# shared modal(s) %> +<%= render partial: 'shared/modal', locals: {:modal_id => 'cite_modal', :title => t('actions.cite'), + :modal_body => @result.cite} %> + +<%# using clipboard.js: https://clipboardjs.com/ %> + +<% if defined?(@request) %> + <% req = render partial: 'shared/request_form' %> + <%= render partial: 'shared/modal', locals: {:modal_id => 'request_modal', :title => t('actions.request'), + :modal_body => req } %> + +<% end %> + diff --git a/public-new/app/views/shared/_multi_notes.html.erb b/public-new/app/views/shared/_multi_notes.html.erb new file mode 100644 index 0000000000..4869e3e3b9 --- /dev/null +++ b/public-new/app/views/shared/_multi_notes.html.erb @@ -0,0 +1,9 @@ +<% types.each do |type| + note_struct = @result.note(type) + if !note_struct['note_text'].blank? %> +
      + <%= render partial: 'shared/single_note', + locals: {:type => type, :note_struct => @result.note(type)} %> +
      + <% end %> +<% end %> diff --git a/public-new/app/views/shared/_navigation.html.erb b/public-new/app/views/shared/_navigation.html.erb new file mode 100644 index 0000000000..c43f4f6587 --- /dev/null +++ b/public-new/app/views/shared/_navigation.html.erb @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/public-new/app/views/shared/_only_facets.html.erb b/public-new/app/views/shared/_only_facets.html.erb new file mode 100644 index 0000000000..5a2e019130 --- /dev/null +++ b/public-new/app/views/shared/_only_facets.html.erb @@ -0,0 +1,11 @@ +<% unless @facets.blank? %> +

      <%= t('search_results.filter.add')%>:

      +
      + <% @facets.each do |type, h| %> +
      <%= t("search_results.filter.#{type}") %>
      + <% h.each do |v, ct| %> +
      <%= get_pretty_facet_value(type,v) %><%= ct %>
      + <% end %> + <% end %> +
      +<% end %> diff --git a/public-new/app/views/shared/_page_actions.html.erb b/public-new/app/views/shared/_page_actions.html.erb new file mode 100644 index 0000000000..b974ee0e06 --- /dev/null +++ b/public-new/app/views/shared/_page_actions.html.erb @@ -0,0 +1,34 @@ +
      +
        + <% ASUtils.wrap($RECORD_PAGE_ACTIONS[record.primary_type]).each do |page_action| %> +
      • + <% if page_action.has_key?('erb_partial') %> + <%= render :partial => page_action.fetch('erb_partial'), :locals => { :record => record } %> + <% else %> + <% label = I18n.t(page_action.fetch('label'), :default => page_action.fetch('label')) %> + <% icon_class = page_action.fetch('icon') %> + <% icon = "" %> + <% link_content = "#{icon}
        #{label}".html_safe %> + + <% if page_action.has_key?('onclick_javascript') %> + <% javascript = page_action.fetch('onclick_javascript') %> + <%= link_to link_content, 'javascript:void(0);', + :class => 'btn btn-default page_action', + :title => label, + :'data-title' => record.display_string, + :'data-uri' => record.uri, + :onclick => javascript %> + <% elsif page_action.has_key?('url_proc') %> + <% proc = page_action.fetch('url_proc') %> + <% url = proc.call(record) %> + <%= link_to link_content, url, + :class => 'btn btn-default page_action', + :title => label %> + <% else %> + + <% end %> + <% end %> +
      • + <% end %> +
      +
      diff --git a/public-new/app/views/shared/_pagination.html.erb b/public-new/app/views/shared/_pagination.html.erb new file mode 100644 index 0000000000..bb5d5fae41 --- /dev/null +++ b/public-new/app/views/shared/_pagination.html.erb @@ -0,0 +1,24 @@ +<% if pager.pages.last > 1 %> +
      + + +
      +<% end %> diff --git a/public-new/app/views/shared/_present_list.html.erb b/public-new/app/views/shared/_present_list.html.erb new file mode 100644 index 0000000000..f829894cec --- /dev/null +++ b/public-new/app/views/shared/_present_list.html.erb @@ -0,0 +1,39 @@ +<% if list.kind_of? Hash + list.each do |k,v| %> +

      <%= k %>

      + + <% end %> +<% else %> + + <% end %> diff --git a/public-new/app/views/shared/_record.html.erb b/public-new/app/views/shared/_record.html.erb new file mode 100644 index 0000000000..a0f6bc0e4c --- /dev/null +++ b/public-new/app/views/shared/_record.html.erb @@ -0,0 +1,21 @@ + + <% unless rec.dates.blank? %> +
      <%= t('resource._public.dates') %>
      + <%= render partial: 'shared/dates', locals: {:dates => rec.dates} %> + <% end %> + <% unless rec.extents.blank? %> +
      <%= t('resource._public.extent') %>
      + <% rec.extents.each do |ext| %> +

      <%= inheritance(ext['_inherited']).html_safe %> + <%= ext['display']%> +

      + <% end %> + <% end %> + +<% %w{ inventory disposition content_description provenance access_date access_restrictions_note use_restrictions_note}.each do |k| %> + + <% if !rec.notes[k].blank? %> + <%= t("enumeration_names.#{k}") %> +

      <%= process_mixed_content(rec.notes[k]) %>

      + <% end %> +<% end %> diff --git a/public-new/app/views/shared/_record_innards.html.erb b/public-new/app/views/shared/_record_innards.html.erb new file mode 100644 index 0000000000..98f91f6cd4 --- /dev/null +++ b/public-new/app/views/shared/_record_innards.html.erb @@ -0,0 +1,83 @@ + + +<% non_folder = %w(langmaterial physdesc accessrestrict userestrict) %> +<% folder = %w(abstract bioghist arrangement physloc custodhist odd acqinfo relatedmaterial separatedmaterial processinfo) %> + <% over = @result.note('scopecontent') %> + <% if over.blank? + over = @result.note('abstract') + folder.shift # remove abstract from in-folder notes + + end %> + <% unless over.blank? %> + <%= render partial: 'shared/single_note', locals: {:type => 'abstract', :note_struct => over, :notitle => true} %> + <% end %> + <% unless @result.dates.blank? %> +

      <%= t('resource._public.dates') %>

      + <%= render partial: 'shared/dates', locals: {:dates => @result.dates} %> + + <% end %> + <% non_folder.each do |type| %> + <% note_struct = @result.note(type) %> + <% unless note_struct['note_text'].blank? %> + <%= render partial: 'shared/single_note', locals: {:type => type, :note_struct => note_struct} %> + <% end %> + <% end %> + + <% unless @result.extents.blank? %> +

      <%= t('resource._public.extent') %>

      + <% @result.extents.each do |ext| %> +

      <%= inheritance(ext['_inherited']).html_safe %> + <%= ext['display']%> +

      + <% end %> + <% end %> + +
      +
      + <% x = render partial: 'shared/multi_notes', locals: {:types => folder} %> + <% unless x.blank? %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('resource._public.additional'), + :p_id => 'add_desc', :p_body => x } %> + <% end %> + <% unless @result.subjects.blank? %> + <% x= render partial: 'shared/present_list', locals: {:list => @result.subjects, :list_clss => 'subjects_list'} %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('subject._plural'), + :p_id => 'subj_list', :p_body => x} %> + <% end %> + <% unless @result.agents.blank? %> + <% x= render partial: 'shared/present_list', locals: {:list => @result.agents, :list_clss => 'agents_list'} %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('pui_agent.related'), + :p_id => 'agent_list', :p_body => x} %> + <% end %> + <% if @result.kind_of?(Resource) && !@result.finding_aid.blank? %> + <% x= render partial: 'resources/finding_aid' %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('resource._public.finding_aid.head'), + :p_id => 'fa', :p_body => x} %> + <% end %> + <% unless @result['json']['container_disp'].blank? || @result['json']['container_disp'].size <2 %> + <% x = render partial: 'shared/present_list', locals: {:list => @result['json']['container_disp'], :list_clss => 'top_containers'} %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('containers'), :p_id => 'cont_list', + :p_body => x} %> + <% end %> + <% unless @result.external_documents.blank? %> + <% x = render partial: 'shared/present_list', locals: {:list => @result.external_documents, :list_clss => 'external_docs'} %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('external_docs'), :p_id => 'ext_doc_list', :p_body => x} %> + <% end %> + <% unless @repo_info.blank? || @repo_info['top']['name'].blank? %> + <% x= render partial: 'repositories/repository_details' %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('repository.details'), + :p_id => 'repo_deets', :p_body => x} %> + <% end %> + <% if @result.kind_of?(Resource) && !@result.related_accessions.blank? %> + <% x = render partial: 'resources/related_accessions', locals: {:accessions => @result.related_accessions, :deaccessions => @result.related_deaccessions} %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('related_accessions'), :p_id => 'related_accessions_list', :p_body => x} %> + <% end %> + <% if @result.kind_of?(Accession) && !@result.related_resources.blank? %> + <% x = render partial: 'accessions/related_resources', locals: {:resources => @result.related_resources} %> + <%= render partial: 'shared/accordion_panel', locals: {:p_title => t('related_resources'), :p_id => 'related_resources_list', :p_body => x} %> + <% end %> +
      +
      + + diff --git a/public-new/app/views/shared/_request_form.html.erb b/public-new/app/views/shared/_request_form.html.erb new file mode 100644 index 0000000000..f283191697 --- /dev/null +++ b/public-new/app/views/shared/_request_form.html.erb @@ -0,0 +1,32 @@ +<%= form_tag("#{AppConfig[:public_prefix]}fill_request", method: 'post', :id => 'request_form') do %> + <%= render partial: 'shared/request_hiddens' %> +
      +
      + <%= label_tag(:user_name, "#{t('request.user_name')} #{t('request.required')}" , :class => 'sr-only') %> +
      + <%= text_field_tag :user_name, nil, :placeholder => t('request.user_name'), :class => "form-control"%> +
      + <%= t('request.required') %> +
      +
      +
      +
      + <%= label_tag(:user_email, "#{t('request.user_email')} #{t('request.required')}", :class => 'sr-only') %> +
      + <%= text_field_tag :user_email, nil, :type => 'email', :placeholder => t('request.user_email'), :class => 'form-control' %> +
      + <%= t('request.required') %> +
      +
      +
      +
      + <%= label_tag(:date, t('request.date'), :class => 'sr-only') %> + <%= text_field_tag :date, nil, :placeholder => t('request.date'), :class => 'form-control' %> +
      +
      + <%= label_tag(:note, t('request.note'), :class => 'sr-only') %> + <%= text_area_tag :note, nil, :rows=> "3", :cols => "25", :placeholder => t('request.note'), :class => 'form-control' %> +
      + +
      +<% end %> diff --git a/public-new/app/views/shared/_request_hiddens.html.erb b/public-new/app/views/shared/_request_hiddens.html.erb new file mode 100644 index 0000000000..e9f1b9cf75 --- /dev/null +++ b/public-new/app/views/shared/_request_hiddens.html.erb @@ -0,0 +1,15 @@ +<%# yield all the "hidden" fields for a request, so it can be used in two placed %> + <% unless @back_url.blank? %> + + <% end %> + <% %i(repo_name repo_uri repo_code request_uri title resource_id resource_name identifier cite hierarchy restrict restriction_ends).each do |s| %> + + <% end %> + + <% %i(top_container_url container barcode location_title location_url machine).each do |s| %> + <% unless !@request.dig(s) %> + <% @request[s].each do |v| %> + + <% end %> + <% end %> + <% end %> diff --git a/public-new/app/views/shared/_request_page_action.html.erb b/public-new/app/views/shared/_request_page_action.html.erb new file mode 100644 index 0000000000..7356898971 --- /dev/null +++ b/public-new/app/views/shared/_request_page_action.html.erb @@ -0,0 +1,11 @@ +<% if defined?(@request) %> + <%= form_tag "#{@request[:request_uri]}/request", :id => 'request_sub' do |f| %> + <%= render partial: 'shared/request_hiddens' %> + + <% end %> +<% else %> + +<% end %> \ No newline at end of file diff --git a/public-new/app/views/shared/_result.html.erb b/public-new/app/views/shared/_result.html.erb new file mode 100644 index 0000000000..56ec94b568 --- /dev/null +++ b/public-new/app/views/shared/_result.html.erb @@ -0,0 +1,54 @@ + <%# any result that is going to be presented in a list %> + <%# Pry::ColorPrinter.pp(result['json'])%> + <% if !props.fetch(:full,false) %> +
      + <% end %> + <%= render partial: 'shared/idbadge', locals: {:result => result, :props => props } %> +
      + <% if !result['parent_institution_name'].blank? %> +
      <%= t('parent_inst') %>: + <%= result['parent_institution_name'] %> +
      + <% end %> + + <% note_struct = result.note('abstract') + if note_struct.blank? + note_struct = result.note('scopecontent') + end + if !note_struct['note_text'].blank? %> +
      <%= note_struct['label'] %> + <%= note_struct['note_text'].html_safe %>
      + <% end %> + <% unless props.fetch(:no_repo, false) %> + <% r_uri = nil + r_name = nil + if result['json']['repository'] && result['json']['repository']['_resolved'] && (!result['json']['repository']['ref'].blank? || !result['json']['repository']['_resolved']['name'].blank?) + r_uri = result['json']['repository']['ref'] || '' + r_name = result['json']['repository']['_resolved']['name'] || '' + elsif result['_resolved_repository'] && result['_resolved_repository']['json'] + r_uri = result['_resolved_repository']['json']['uri'] || '' + r_name = result['_resolved_repository']['json']['name'] || '' + end + %> + <% unless !r_uri && !r_name %> +
      <%= t('context') %>: + + <%= r_name%> + <% unless props.fetch(:no_res, false) || result['_resolved_resource'].blank? || result['_resolved_resource']['json'].blank? || !result['_resolved_resource']['json']['publish'] %> + / + <%= process_mixed_content(result['_resolved_resource']['json']['title']).html_safe %> + + <% end %> +
      + <% end %> + <% end %> + + + <% if !props.fetch(:full,false) && result['primary_type'] == 'repository' %> +
      <%= t('number_of', { :type => t('resource._plural') }) %> <%= @counts[result.uri]['resource'] %>
      + <% end %> +
      + + <% if !props.fetch(:full,false) %> +
      + <% end %> diff --git a/public-new/app/views/shared/_search.html.erb b/public-new/app/views/shared/_search.html.erb new file mode 100644 index 0000000000..21f8ff76b8 --- /dev/null +++ b/public-new/app/views/shared/_search.html.erb @@ -0,0 +1,43 @@ + + diff --git a/public-new/app/views/shared/_single_note.html.erb b/public-new/app/views/shared/_single_note.html.erb new file mode 100644 index 0000000000..f1f9f3c9ba --- /dev/null +++ b/public-new/app/views/shared/_single_note.html.erb @@ -0,0 +1,8 @@ +<% if !note_struct['note_text'].blank? %> +
      + <% unless defined?(notitle) && notitle %> +

      <%= note_struct['label'] %>

      + <% end %> + <%= note_struct['note_text'].html_safe %> +
      +<% end %> diff --git a/public-new/app/views/shared/_sorter.html.erb b/public-new/app/views/shared/_sorter.html.erb new file mode 100644 index 0000000000..66984b2edf --- /dev/null +++ b/public-new/app/views/shared/_sorter.html.erb @@ -0,0 +1,13 @@ + +<% if defined?(@sort) && defined?(@sort_opts) && defined?(@base_search) %> + + +
      + <%= form_tag("#{@base_search}", method: 'get', :class=> "form-inline") do %> + <%= render partial: 'shared/hidden_params' %> + <%= label_tag(:sort, "#{t('search_sorting.sort_by')}", :class=>'sr-only') %> + <%= select_tag(:sort, options_for_select(@sort_opts,@sort)) %> + <%= submit_tag(t('search_sorting.sort'), :class=>'btn btn-primary btn-sm') %> + <% end %> +
      +<% end %> diff --git a/public-new/app/views/shared/not_found.html.erb b/public-new/app/views/shared/not_found.html.erb new file mode 100644 index 0000000000..a0a7943de1 --- /dev/null +++ b/public-new/app/views/shared/not_found.html.erb @@ -0,0 +1,11 @@ +
      + + +
      +

      <%= t('errors.error_404_message', :type => @type) %>

      +

      <%= @uri %>

      + <% unless @back_url.blank? %> +

      <%= t('actions.backup') %>

      + <% end %> +
      +
      diff --git a/public-new/app/views/site/index.html.erb b/public-new/app/views/site/index.html.erb deleted file mode 100644 index 332dc7e556..0000000000 --- a/public-new/app/views/site/index.html.erb +++ /dev/null @@ -1,765 +0,0 @@ - - - - - - <%= stylesheet_link_tag "application" %> - - - - - -
      - - -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      - - <%= javascript_include_tag "application" %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public-new/app/views/subjects/show.html.erb b/public-new/app/views/subjects/show.html.erb new file mode 100644 index 0000000000..cd8c323921 --- /dev/null +++ b/public-new/app/views/subjects/show.html.erb @@ -0,0 +1,62 @@ +
      + +
      + <%= render partial: 'shared/idbadge', locals: {:result => @result, :props => { :full => true} } %> + + + +
      + <%= t('enumeration_names.subject_source') %>: <%= t("enumerations.subject_source.#{@result['json']['source']}" ) %> +
      + + +
      +<% unless @result['json']['used_within_repositories'].blank? %> + <%= t('subject.used_within') %>: + +<% end %> + <% if !@result.dates.blank? %> + <% render partial: 'shared/dates', locals: {:dates => @result.dates} %> + <% end %> + + <% if !@result['json']['component_id'].blank? %> + <%= t(component._singular) %>: <%= @result['json']['component_id'] %> + <% end %> + <% if !@result['json']['ref_id'].blank? %> + [Ref. ID: <%= @result['json']['ref_id'] %>] + <% end %> + <% if @result['json']['scope_note'].present? %> + <%= t('scope_note') %>: <%= process_mixed_content( @result['json']['scope_note']).html_safe %> + <% end %> + <%= render partial: 'shared/display_notes' %> + <% unless @results.blank? || @results['total_hits'] == 0 %> +

      <%= t('found', {:count => @results['total_hits'], :type => @results['total_hits'] == 1? t('coll_obj._singular') : t('coll_obj._plural')}) %>:

      + <% @results.records.each do |result| %> + <%= render partial: 'shared/result', locals: {:result => result, :props => {:full => false}} %> + <% end %> + <%= render partial: 'shared/pagination', locals: {:pager => @pager} %> + + <% end %> + +
      + +
      diff --git a/public-new/app/views/welcome/show.html.erb b/public-new/app/views/welcome/show.html.erb new file mode 100644 index 0000000000..2224ed314a --- /dev/null +++ b/public-new/app/views/welcome/show.html.erb @@ -0,0 +1,20 @@ + +
      +
      +

      <%= t('brand.welcome_head') %>

      +
      <%= t('brand.welcome_message').html_safe %>
      +
      +
      + + <%= render partial: 'shared/search', locals: {:search_url => "/search", + :title => t('archive._plural'), + :limit_options => [["#{t('actions.search')} #{t('search-limits.all')}",''], + [t('search-limit', :limit => t('search-limits.resources')),'resource'], + [t('search-limit', :limit => t('search-limits.digital')),'digital_object']], + :field_options => [["#{t('search_results.filter.fullrecord')}",''], + ["#{t('search_results.filter.title')}",'title'], + ["#{t('search_results.filter.creators')}",'creators_text'], + ["#{t('search_results.filter.notes')}", 'notes'] ] } %> + + + diff --git a/public-new/bin/bundle b/public-new/bin/bundle index 66e9889e8b..ae2d3fdc83 100755 --- a/public-new/bin/bundle +++ b/public-new/bin/bundle @@ -1,3 +1,3 @@ -#!/usr/bin/env ruby +#!/usr/bin/env jruby ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) load Gem.bin_path('bundler', 'bundle') diff --git a/public-new/bin/rails b/public-new/bin/rails index 5191e6927a..bb2000cb45 100755 --- a/public-new/bin/rails +++ b/public-new/bin/rails @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +#!/usr/bin/env jruby +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/public-new/bin/rake b/public-new/bin/rake index 17240489f6..6dffe323e5 100755 --- a/public-new/bin/rake +++ b/public-new/bin/rake @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +#!/usr/bin/env jruby require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/public-new/bin/setup b/public-new/bin/setup index acdb2c1389..c2cb056c9d 100755 --- a/public-new/bin/setup +++ b/public-new/bin/setup @@ -1,29 +1,34 @@ -#!/usr/bin/env ruby +#!/usr/bin/env jruby require 'pathname' +require 'fileutils' +include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) -Dir.chdir APP_ROOT do +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do # This script is a starting point to setup your application. - # Add necessary setup steps to this file: + # Add necessary setup steps to this file. - puts "== Installing dependencies ==" - system "gem install bundler --conservative" - system "bundle check || bundle install" + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # system "cp config/database.yml.sample config/database.yml" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" - system "rm -f log/*" - system "rm -rf tmp/cache" + system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" - system "touch tmp/restart.txt" + system! 'bin/rails restart' end diff --git a/public-new/bin/update b/public-new/bin/update new file mode 100755 index 0000000000..a0c2e4dc91 --- /dev/null +++ b/public-new/bin/update @@ -0,0 +1,29 @@ +#!/usr/bin/env jruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/public-new/bower.json b/public-new/bower.json deleted file mode 100644 index 1ec8442550..0000000000 --- a/public-new/bower.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "archivesspace_public", - "description": "ArchivesSpace Public Interface", - "main": "index.js", - "authors": [ - "Brian Hoffman " - ], - "license": "ISC", - "homepage": "https://github.com/archivesspace/archivesspace", - "moduleType": [], - "private": true, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "dependencies": { - "exoskeleton": "~0.7.0", - "jquery": "~2.2.0", - "lodash": "~4.0.0", - "jquery.scrollTo": "~2.1.2", - "load-awesome": "~1.1.0", - "foundation-icon-fonts": "*", - "foundation-sites": "~6.1.1", - "lodash-inflection": "~1.3.2", - "jstree": "git@github.com:vakata/jstree#~3.3.1", - "bootstrap-sass": "~3.3.6" - }, - "install": { - "path": { - "js": "vendor/assets/javascripts", - "css": "vendor/assets/stylesheets", - "scss": "vendor/assets/stylesheets", - "map": "vendor/assets/stylesheets", - "eot": "vendor/assets/fonts", - "ttf": "vendor/assets/fonts", - "woff": "vendor/assets/fonts", - "svg": "vendor/assets/fonts" - }, - "sources": { - "exoskeleton": [ - "bower_components/exoskeleton/exoskeleton.js" - ], - "jquery": { - "mapping": [ - { - "bower_components/jquery/dist/jquery.js": "jquery.js" - } - ] - }, - "lodash": { - "mapping": [ - { - "bower_components/lodash/lodash.js": "lodash.js" - } - ] - }, - "foundation-sites": [ - "bower_components/foundation-sites/js/**", - "bower_components/foundation-sites/scss/**" - ], - "backbone.paginator": { - "mapping": [ - { - "bower_components/backbone.paginator/lib/backbone.paginator.js": "backbone.paginator.js" - } - ] - }, - "load-awesome": [ - "bower_components/load-awesome/css/ball-pulse-rise.css", - "bower_components/load-awesome/css/ball-fall.css", - "bower_components/load-awesome/css/line-spin-clockwise-fade.css" - ], - "foundation-icon-fonts": [ - "bower_components/foundation-icon-fonts/_foundation-icons.scss", - "bower_components/foundation-icon-fonts/foundation-icons.*" - ], - "bootstrap-sass": [ - "bower_components/bootstrap-sass/assets/fonts/bootstrap/*", - "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/_glyphicons.scss" - ] - } - }, - "resolutions": { - "lodash": "~4.0.0" - } -} diff --git a/public-new/config.ru b/public-new/config.ru index e0835e018d..f7ba0b527b 100644 --- a/public-new/config.ru +++ b/public-new/config.ru @@ -1,7 +1,5 @@ # This file is used by Rack-based servers to start the application. -require "aspace_gems" -ASpaceGems.setup +require_relative 'config/environment' -require ::File.expand_path('../config/environment', __FILE__) run Rails.application diff --git a/public-new/config/application.rb b/public-new/config/application.rb index c38a788d17..5dd2f4da61 100644 --- a/public-new/config/application.rb +++ b/public-new/config/application.rb @@ -1,22 +1,20 @@ -require File.expand_path('../boot', __FILE__) - -require "rails" -# Pick the frameworks you want: -# require "active_model/railtie" -# require "active_job/railtie" -# require "active_record/railtie" -require "action_controller/railtie" -require "action_mailer/railtie" -# require "action_view/railtie" -require "sprockets/railtie" -require "rails/test_unit/railtie" - -require 'java' -require 'config/config-distribution' +require_relative 'boot' + +require 'rails' +require 'action_controller/railtie' +require 'action_view/railtie' +require 'sprockets/railtie' +require 'action_mailer/railtie' + require 'asutils' +require_relative 'initializers/plugins' -require "rails_config_bug_workaround" +# Maybe we won't need these? +# DISABLED BY MST # require 'active_record/railtie' +# DISABLED BY MST # require 'active_job/railtie' +# DISABLED BY MST # require 'action_cable/engine' +# DISABLED BY MST # require 'rails/test_unit/railtie' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -28,56 +26,60 @@ class Application < Rails::Application # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de - - config.assets.enabled = true - config.assets.paths << Rails.root.join('vendor', 'assets', 'fonts') - - - # config.paths["app/controllers"].concat(ASUtils.find_local_directories("public/controllers")) - - config.action_controller.relative_url_root = AppConfig[:public_proxy_prefix].sub(/\/$/, '') - - - config.i18n.default_locale = AppConfig[:locale] + # Add plugin controllers and models + config.paths["app/controllers"].concat(ASUtils.find_local_directories("public/controllers")) + config.paths["app/models"].concat(ASUtils.find_local_directories("public/models")) # Load the shared 'locales' - ASUtils.find_locales_directories.map{|locales_directory| File.join(locales_directory)}.reject { |dir| !Dir.exists?(dir) }.each do |locales_directory| - config.i18n.load_path += Dir[File.join(locales_directory, '**' , '*.{rb,yml}')] + ASUtils.find_locales_directories.map{|locales_directory| File.join(locales_directory)}.reject { |dir| !Dir.exist?(dir) }.each do |locales_directory| + I18n.load_path += Dir[File.join(locales_directory, '**' , '*.{rb,yml}')] end - # Override with any local locale files - config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] + I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] - # Allow overriding of the i18n locales via the local folder(s) + # Allow overriding of the i18n locales via the 'local' folder(s) if not ASUtils.find_local_directories.blank? - ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'locales')}.reject { |dir| !Dir.exists?(dir) }.each do |locales_override_directory| - config.i18n.load_path += Dir[File.join(locales_override_directory, '**' , '*.{rb,yml}')] + ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'locales')}.reject { |dir| !Dir.exist?(dir) }.each do |locales_override_directory| + I18n.load_path += Dir[File.join(locales_override_directory, '**' , '*.{rb,yml}')] end end - config.encoding = "utf-8" - - config.filter_parameters += [:password] + # Add template static assets to the path + if not ASUtils.find_local_directories.blank? + ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'assets')}.reject { |dir| !Dir.exist?(dir) }.each do |static_directory| + config.assets.paths.unshift(static_directory) + end + end - config.active_support.escape_html_entities_in_json = true + # add fonts to the asset path + config.assets.paths << Rails.root.join("app", "assets", "fonts") - config.assets.enabled = true + # mailer configuration + if AppConfig[:pui_email_enabled] + config.action_mailer.delivery_method = AppConfig[:pui_email_delivery_method] + config.action_mailer.perform_deliveries = AppConfig[:pui_email_perform_deliveries] + config.action_mailer.raise_delivery_errors = AppConfig[:pui_email_raise_delivery_errors] - AppConfig.load_into(config) - end + if config.action_mailer.delivery_method == :sendmail + if AppConfig.has_key? :pui_email_sendmail_settings + config.action_mailer.smtp_settings = AppConfig[:pui_email_sendmail_settings] + end + end + if config.action_mailer.delivery_method == :smtp + config.action_mailer.smtp_settings = AppConfig[:pui_email_smtp_settings] + end + else + config.action_mailer.delivery_method = :test + config.action_mailer.perform_deliveries = false + end - class SessionGone < StandardError end +end - - class SessionExpired < StandardError +# Load plugin init.rb files (if present) +ASUtils.find_local_directories('public').each do |dir| + init_file = File.join(dir, "plugin_init.rb") + if File.exist?(init_file) + load init_file end - end diff --git a/public-new/config/boot.rb b/public-new/config/boot.rb index 1c729bbe53..67c447d556 100644 --- a/public-new/config/boot.rb +++ b/public-new/config/boot.rb @@ -1,9 +1,10 @@ require 'rubygems' require 'stringio' +# Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'aspace_gems' ASpaceGems.setup -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/public-new/config/database.yml b/public-new/config/database.yml new file mode 100644 index 0000000000..28c36eb82f --- /dev/null +++ b/public-new/config/database.yml @@ -0,0 +1,23 @@ +# SQLite version 3.x +# gem 'activerecord-jdbcsqlite3-adapter' +# +# Configure Using Gemfile +# gem 'activerecord-jdbcsqlite3-adapter' +# +default: &default + adapter: sqlite3 + +development: + <<: *default + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/public-new/config/environment.rb b/public-new/config/environment.rb index ee8d90dc65..426333bb46 100644 --- a/public-new/config/environment.rb +++ b/public-new/config/environment.rb @@ -1,5 +1,5 @@ # Load the Rails application. -require File.expand_path('../application', __FILE__) +require_relative 'application' # Initialize the Rails application. Rails.application.initialize! diff --git a/public-new/config/environments/development.rb b/public-new/config/environments/development.rb index 0feba2ef19..968521eee1 100644 --- a/public-new/config/environments/development.rb +++ b/public-new/config/environments/development.rb @@ -7,32 +7,48 @@ config.cache_classes = false # Do not eager load code on boot. - config.eager_load = false + config.eager_load = true - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=172800' + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end # Don't care if the mailer can't send. - config.action_mailer.raise_delivery_errors = false + # DISABLED BY MST # config.action_mailer.raise_delivery_errors = false + + # DISABLED BY MST # config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Raise an error on page load if there are pending migrations. + # DISABLED BY MST # config.active_record.migration_error = :page_load + # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true + # Suppress logger output for asset requests. + config.assets.quiet = true # Raises error for missing translations # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker end diff --git a/public-new/config/environments/production.rb b/public-new/config/environments/production.rb index a008b61074..05c80300c3 100644 --- a/public-new/config/environments/production.rb +++ b/public-new/config/environments/production.rb @@ -14,15 +14,9 @@ config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like - # NGINX, varnish or squid. - # config.action_dispatch.rack_cache = true - # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier @@ -31,16 +25,20 @@ # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Asset digests allow you to set far-future HTTP expiration dates on all assets, - # yet still be able to expire them through the digest params. - config.assets.digest = true - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -49,20 +47,19 @@ config.log_level = :debug # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "archivesspace-public_#{Rails.env}" + # DISABLED BY MST # config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false + # DISABLED BY MST # # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). @@ -73,4 +70,17 @@ # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + # DISABLED BY MST # config.active_record.dump_schema_after_migration = false end diff --git a/public-new/config/environments/test.rb b/public-new/config/environments/test.rb index 1c19f08b28..0e5b51bdc6 100644 --- a/public-new/config/environments/test.rb +++ b/public-new/config/environments/test.rb @@ -12,9 +12,11 @@ # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static file server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } # Show full error reports and disable caching. config.consider_all_requests_local = true @@ -25,14 +27,12 @@ # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + # DISABLED BY MST # config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. - config.action_mailer.delivery_method = :test - - # Randomize the order test cases are executed. - config.active_support.test_order = :random + # DISABLED BY MST # config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr diff --git a/public-new/config/initializers/application_controller_renderer.rb b/public-new/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..51639b67a0 --- /dev/null +++ b/public-new/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/public-new/config/initializers/assets.rb b/public-new/config/initializers/assets.rb index 01ef3e6630..944d436138 100644 --- a/public-new/config/initializers/assets.rb +++ b/public-new/config/initializers/assets.rb @@ -9,3 +9,4 @@ # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # Rails.application.config.assets.precompile += %w( search.js ) +Rails.application.config.assets.precompile += %w( infinite_scroll.js largetree.js treesync.js tree_renderer.js ) diff --git a/public-new/config/initializers/cookies_serializer.rb b/public-new/config/initializers/cookies_serializer.rb index 7f70458dee..5a6a32d371 100644 --- a/public-new/config/initializers/cookies_serializer.rb +++ b/public-new/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/public-new/config/initializers/email_overrides.rb b/public-new/config/initializers/email_overrides.rb new file mode 100644 index 0000000000..8e1fd9f5ca --- /dev/null +++ b/public-new/config/initializers/email_overrides.rb @@ -0,0 +1,11 @@ +class EmailOverrides + + def self.delivering_email(mail) + if AppConfig.has_key?('pui_email_override') + mail.to = AppConfig['pui_email_override'] + end + end + +end + +ActionMailer::Base.register_interceptor(EmailOverrides) \ No newline at end of file diff --git a/public-new/config/initializers/exceptions.rb b/public-new/config/initializers/exceptions.rb new file mode 100644 index 0000000000..d7b8f1b8b0 --- /dev/null +++ b/public-new/config/initializers/exceptions.rb @@ -0,0 +1,2 @@ +# load common/exceptions.rb +require 'exceptions' \ No newline at end of file diff --git a/public-new/config/initializers/json_object.rb b/public-new/config/initializers/json_object.rb deleted file mode 100644 index b30b8395dc..0000000000 --- a/public-new/config/initializers/json_object.rb +++ /dev/null @@ -1,46 +0,0 @@ -require "jsonmodel" -require "memoryleak" -require "client_enum_source" -require "jsonmodel_translatable" -require "jsonmodel_publishing" - -if not ENV['DISABLE_STARTUP'] - while true - begin - JSONModel::init(:client_mode => true, - :priority => :high, - :mixins => [ - JSONModelTranslatable, - JSONModelPublishing - ], - :enum_source => ClientEnumSource.new, - :i18n_source => I18n, - :url => AppConfig[:backend_url]) - break - rescue - $stderr.puts "Connection to backend failed. Retrying..." - sleep(5) - end - end - - MemoryLeak::Resources.define(:repository, proc { JSONModel(:repository).all }, 60) - - JSONModel::Notification::add_notification_handler("REPOSITORY_CHANGED") do |msg, params| - MemoryLeak::Resources.refresh(:repository) - end - - JSONModel::Notification.start_background_thread - - JSONModel::add_error_handler do |error| - if error["code"] == "SESSION_GONE" - raise ArchivesSpacePublic::SessionGone.new("Your backend session was not found") - end - if error["code"] == "SESSION_EXPIRED" - raise ArchivesSpacePublic::SessionExpired.new("Your session expired due to inactivity. Please sign in again.") - end - end - -end - - -include JSONModel diff --git a/public-new/config/initializers/new_framework_defaults.rb b/public-new/config/initializers/new_framework_defaults.rb new file mode 100644 index 0000000000..1dd4df730f --- /dev/null +++ b/public-new/config/initializers/new_framework_defaults.rb @@ -0,0 +1,24 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Read the Rails 5.0 release notes for more info on each option. + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = true + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = true + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = true + +# Require `belongs_to` associations by default. Previous versions had false. +# DISABLED BY MST # Rails.application.config.active_record.belongs_to_required_by_default = true + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = false + +# Configure SSL options to enable HSTS with subdomains. Previous versions had false. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } diff --git a/public-new/config/initializers/plugins.rb b/public-new/config/initializers/plugins.rb new file mode 100644 index 0000000000..5deaecbce5 --- /dev/null +++ b/public-new/config/initializers/plugins.rb @@ -0,0 +1,30 @@ +module Plugins + + def self.extend_aspace_routes(routes_file) + ArchivesSpacePublic::Application.config.paths['config/routes.rb'].concat([routes_file]) + end + + def self.add_menu_item(path, label, position = nil) + ArchivesSpacePublic::Application.config.after_initialize do + PublicNewDefaults::add_menu_item(path, label, position) + end + end + + def self.add_record_page_action_proc(record_type, label, icon_css, build_url_proc, position = nil) + ArchivesSpacePublic::Application.config.after_initialize do + PublicNewDefaults::add_record_page_action_proc(record_type, label, icon_css, build_url_proc, position) + end + end + + def self.add_record_page_action_js(record_type, label, icon_css, onclick_javascript, position = nil) + ArchivesSpacePublic::Application.config.after_initialize do + PublicNewDefaults::add_record_page_action_js(record_type, label, icon_css, onclick_javascript, position) + end + end + + def self.add_record_page_action_erb(record_types, erb_partial, position = nil) + ArchivesSpacePublic::Application.config.after_initialize do + PublicNewDefaults::add_record_page_action_erb(record_types, erb_partial, position) + end + end +end diff --git a/public-new/config/initializers/public_new_defaults.rb b/public-new/config/initializers/public_new_defaults.rb new file mode 100644 index 0000000000..2678639b94 --- /dev/null +++ b/public-new/config/initializers/public_new_defaults.rb @@ -0,0 +1,151 @@ +#require "memoryleak" +# require 'pp' +module PublicNewDefaults +# pp "initializing resources" +# FIXME do we need to do this in the intializer? +# Repository.set_repos(ArchivesSpaceClient.new.list_repositories) + +# determining the main menu + $MAIN_MENU = [] + AppConfig[:pui_hide].keys.each do |k| + unless AppConfig[:pui_hide][k] + case k + when :repositories + $MAIN_MENU.push(['/repositories', 'repository._plural']) + when :resources + $MAIN_MENU.push(['/repositories/resources', 'resource._plural']) + when :digital_objects + $MAIN_MENU.push(['/objects?limit=digital_object', 'digital_object._plural' ]) + when :accessions + $MAIN_MENU.push(['/accessions', 'unprocessed']) + when :subjects + $MAIN_MENU.push(['/subjects', 'subject._plural']) + when :agents + $MAIN_MENU.push(['/agents', 'pui_agent._plural']) + when :classifications + $MAIN_MENU.push(['/classifications', 'classification._plural']) + end + end + end + + def self.add_menu_item(path, label, position = nil) + if position.nil? + $MAIN_MENU.push([path, label]) + else + $MAIN_MENU.insert(position, [path, label]) + end + end +# Pry::ColorPrinter.pp $MAIN_MENU +# MemoryLeak::Resources.define(:repository, proc { ArchivesSpaceClient.new.list_repositories }, 60) +#pp MemoryLeak::Resources.get(:repository) + + # Setup the Page Action menu items + $RECORD_PAGE_ACTIONS = {} + + # Add a link page action for a particular jsonmodel record type + # - record_types: the types to display for e.g, resource, archival_object etc + # - label: I18n path or string for the label + # - icon: CSS classes for the Font Awesome icon e.g. 'fa-book' + # - url_proc: a proc passed the record upon render which must return a string + # (the record is passed as a param to the proc) + # - position: index to include the menu item + def self.add_record_page_action_proc(record_types, label, icon, url_proc, position = nil) + action = { + 'label' => label, + 'icon' => icon, + 'url_proc' => url_proc + } + + ASUtils.wrap(record_types).each do |record_type| + $RECORD_PAGE_ACTIONS[record_type] ||= [] + if (position.nil?) + $RECORD_PAGE_ACTIONS[record_type] << action + else + $RECORD_PAGE_ACTIONS[record_type].insert(position, action) + end + end + end + + # Add a JavaScript page action for a particular jsonmodel record type + # - record_types: the types to display for e.g, resource, archival_object etc + # - label: I18n path or string for the label + # - icon: CSS classes for the Font Awesome icon e.g. 'fa fa-book fa-3x' + # - onclick_javascript: a javascript expression to run when the action is clicked + # (the record uri and title are available as data attributes on the button element) + # - position: index to include the menu item + def self.add_record_page_action_js(record_types, label, icon, onclick_javascript, position = nil) + action = { + 'label' => label, + 'icon' => icon, + 'onclick_javascript' => onclick_javascript + } + + ASUtils.wrap(record_types).each do |record_type| + $RECORD_PAGE_ACTIONS[record_type] ||= [] + if (position.nil?) + $RECORD_PAGE_ACTIONS[record_type] << action + else + $RECORD_PAGE_ACTIONS[record_type].insert(position, action) + end + end + end + +# Add an action from an ERB for a particular jsonmodel record type +# - record_types: the types to display for e.g, resource, archival_object etc +# - erb_partial: the path the erb partial +# - position: index to include the menu item + def self.add_record_page_action_erb(record_types, erb_partial, position = nil) + action = { + 'erb_partial' => erb_partial, + } + + ASUtils.wrap(record_types).each do |record_type| + $RECORD_PAGE_ACTIONS[record_type] ||= [] + if (position.nil?) + $RECORD_PAGE_ACTIONS[record_type] << action + else + $RECORD_PAGE_ACTIONS[record_type].insert(position, action) + end + end + end + + ## Load any default actions: + # Cite + if AppConfig[:pui_page_actions_cite] + add_record_page_action_erb(['resource', 'archival_object', 'digital_object', 'digital_object_component'], + 'shared/cite_page_action') + end + + ## Bookmark + # TODO disabled for now; to be implemented with the bookbag feature + # if AppConfig[:pui_page_actions_bookmark] + # add_record_page_action_js(['resource', 'archival_object', 'digital_object', 'digital_object_component'], + # 'actions.bookmark', + # 'fa-bookmark', + # 'bookmark_page()') + # end + + # Request + if AppConfig[:pui_page_actions_request] + add_record_page_action_erb(['resource', 'archival_object', 'digital_object', 'digital_object_component'], + 'shared/request_page_action') + end + + ## Print + # FIXME disabled for now; to be implemented with the offline PDF finding aids AR-1471 + # if AppConfig[:pui_page_actions_print] + # add_record_page_action_js(['resource', 'archival_object', 'digital_object', 'digital_object_component'], + # 'actions.print', + # 'fa-file-pdf-o', + # 'print_page()') + # end + + # Load any custom actions defined in AppConfig: + ASUtils.wrap(AppConfig[:pui_page_custom_actions]).each do |action| + ASUtils.wrap(action.fetch('record_type')).each do |record_type| + $RECORD_PAGE_ACTIONS[record_type] ||= [] + $RECORD_PAGE_ACTIONS[record_type] << action + end + end + +end diff --git a/public-new/config/initializers/secret_token.rb b/public-new/config/initializers/secret_token.rb new file mode 100644 index 0000000000..1eef89e8ae --- /dev/null +++ b/public-new/config/initializers/secret_token.rb @@ -0,0 +1,6 @@ +require 'digest/sha1' + +if !ENV['DISABLE_STARTUP'] + ArchivesSpacePublic::Application.config.secret_token = Digest::SHA1.hexdigest(AppConfig[:public_cookie_secret]) + ArchivesSpacePublic::Application.config.secret_key_base = Digest::SHA1.hexdigest(AppConfig[:public_cookie_secret]) +end diff --git a/public-new/config/initializers/session_store.rb b/public-new/config/initializers/session_store.rb index 28e99cf6f8..56fe0be09c 100644 --- a/public-new/config/initializers/session_store.rb +++ b/public-new/config/initializers/session_store.rb @@ -1,8 +1,3 @@ # Be sure to restart your server when you modify this file. -ArchivesSpacePublic::Application.config.session_store :cookie_store, key: "#{AppConfig[:cookie_prefix]}_public_session" - -# Use the database for sessions instead of the cookie-based default, -# which shouldn't be used to store highly confidential information -# (create the session table with "rails generate session_migration") -# Public::Application.config.session_store :active_record_store +Rails.application.config.session_store :cookie_store, key: '_archivesspace-public_session' diff --git a/public-new/config/initializers/wrap_parameters.rb b/public-new/config/initializers/wrap_parameters.rb index b81ea74b77..bbfc3961bf 100644 --- a/public-new/config/initializers/wrap_parameters.rb +++ b/public-new/config/initializers/wrap_parameters.rb @@ -5,5 +5,10 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/public-new/config/locales/en.yml b/public-new/config/locales/en.yml index c61f1a6db5..817ab750c5 100644 --- a/public-new/config/locales/en.yml +++ b/public-new/config/locales/en.yml @@ -1,16 +1,92 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + en: brand: - title: ArchivesSpace Public Interface + title: ASpace Public Interface + title_link_text: Your link text here home: Home - welcome_message: Welcome to ArchivesSpace. + welcome_head: Welcome to the New Public Interface for ArchivesSpace + welcome_message: | +

      You can put anything you want in this space, by + editing brand.welcome_message in the appropriate config/custom/locales/*yml file

      + welcome_search_label: "Find what you're looking for:" + welcome_page_title: Welcome! A New Day Dawns! actions: search: Search + search_within: Search within results + search_in: "Search %{type}" browse: Browse show_advanced_search: Show Advanced Search hide_advanced_search: Hide Advanced Search + backup: Return to the previous page + backup_abbr: Go back + cite: Citation + bookmark: Bookmark + request: Request + print: Print + collection_overview: Collection Overview + hierarch: Collection Organization + numeric: Container Inventory + detailed: Detailed Contents + record_details: Record Details + clipboard: Copy to clipboard + + page_actions: Page Actions + request: + required: required + user_name: Your name + user_email: Your email address + date: Anticipated arrival date + note: Note to the staff + visit: "Submit a request to visit %{repo_name} to view %{archival_object}" + submit: Submit Request + submitted: Thank you, your request has been submitted. You will soon recieve an email with a copy of your request. + errors: + name: Please enter your name + email: Please enter a valid email address + email: + subject: "Request for %{title}" + + search-button: + label: Search + search-limit: "Limit to %{limit}" + search-limiting: "limited to %{limit}" + search-limits: + all: all record types + repository: repositories + repositories: repositories + resource: collections + resources: collections + digital: digital materials + digital_object: digital materials + objects: archival or digital records + subject: subject + agent: content creator + agents: content creators + search-field: Search field + searched-field: "searching %{field}" pagination: first: First @@ -25,13 +101,29 @@ en: page_connector: of search_results: + page_title: "Found %{count} Results" + results_head: "Showing %{type}: %{start} - %{end} of %{total}" + results: Results + head_prefix: Found + head_suffix: Results searching: Searching + search_for: "Search %{type} where %{conditions}" + keyword_contain: "keyword(s): %{kw}" + title_contain: "the title contains %{kw}" + notes_contain: "the note contains %{kw}" + creators_text_contain: "the creator contains %{kw}" + in_repository: " in %{name}" + year_range: " from %{from_year} to %{to_year}" + anything: anything + search_term: Search term title: All Records - no_results: No Records + no_results: No Records Found no_results_prefix: There were no search results for result_type: Record Type result_title: Title + filter_by: Filter By filtered_by: Filtered By + remove_filter: Remove this filter modified: Last Modified created: Created text_placeholder: Filter by text @@ -39,13 +131,21 @@ en: created: Created modified: Modified component_switch: Include Components + op: + AND: and + OR: or + NOT: and not filter: + head: Filter Results + add: Additional filters + form: Searching within results show_fewer: Show fewer show_more: Show more query: Text subjects: Subject primary_type: Type repository: Repository + used_within_repository: Repository creators: Creator fullrecord: Keyword subjects_text: Subject @@ -54,16 +154,30 @@ en: scope: Scope notes: Notes source: Source - agents: Agents - agents_text: Agents + agents: Content Creators + agents_text: Content Creators classification_uri: Classification linked_agent_roles: Role + from_year: From year + to_year: To year + years: Years + from: From + to: To + years_include: Years to include + year_now: now + year_begin: the beginning + from_to: "Years: %{begin} to %{end}" + child_container_u_sstr: Child container + grand_child_container_u_sstr: Grandchild container + type_enum_s: Type + series_title_u_sstr: Series search_sorting: sort_by: "Sort by:" + sort: Sort and: "and:" - asc: Ascending - desc: Descending + asc: Asc. + desc: Desc. relevance: Relevance title_sort: Title user_mtime: Modified @@ -79,10 +193,26 @@ en: event_type: Type identifier: Identifier select: Select + year_sort: Year + sorting: "%{type} %{direction}" + inherit: + inherited: "From the %{level}:" + collection: Collection + series: Series + sub-series: Sub-Series + class: Class + file: File + fonds: Fonds + item: Item + otherlevel: Other Level + recordgrp: Record Group + subfonds: Sub-Fonds + subgrp: Sub-Group + errors: - error_404: Record Not Found - error_404_message: "The record you've tried to access does not exist or may have been removed." + error_404: "%{type} Record Not Found" + error_404_message: "The %{type} record you've tried to access does not exist or may have been removed." backend_down: Unable to Connect to Database backend_down_message: ArchivesSpace is currently experiencing technical difficulties. We apologise for the inconvenience. Please try again later. @@ -93,8 +223,21 @@ en: navbar: search_placeholder: Enter your search terms search_all: All Records + error_no_term: No search term(s) entered. + search_terms_results: "Your search for {{search_terms}} yielded {{number}} results" + + internal_links: + main: Main Content + filter: Filter Results + collection: Collection Organization + search_collection: Search within collection + + accordion: + expand: Expand All + collapse: Collapse All advanced_search: + operator_label: Search operator operator: AND: And OR: Or @@ -108,35 +251,126 @@ en: show: Show Advanced Search hide: Hide Advanced Search + contains: "Contains %{count} %{type}" + found: "Found in %{count} %{type}" + coll_obj: + _singular: Collection or Record + _plural: Collections and/or Records + + # I don't have a place for these, so I'm putting them here + scope_note : Scope Note + multiple_containers: Multiple Containers + # adding the inherited title to the display string + inherited: "%{title}, %{display}" + extent_number_type: "%{number} %{type}" + extent_phys_details: ": %{deets}" + extent_dims: ": %{dimensions}" + + external_docs: External Documents + more_about: More about + listing: Listing + list: "%{type} List" + cont_arr: Collection organization + subgroups: Subgroups + accessed: Accessed + containers: Container List + related_accessions: Related Accessions + related_resources: Related Resources + accession: - _public: + _singular: Unprocessed + _plural: Instances of Unprocessed Material + _public: section: summary: Summary + related_accessions: + accessions: Accession Records + deaccessions: Deaccession Records archival_object: + _singular: Archival Record + _plural: Archival Records _public: section: components: Components summary: Summary messages: - no_components: Archival Object has no components + no_components: Archival Record has no components + header: + dates: Dates + lang: Language + physdesc: Physical Description + storage: Container Information + access: Conditions Governing Access + use: Conditions Governing Use + component: Component + ref_id: Ref. ID + + archive: + _singular: The Archive + _plural: The Archives + + repository: + _singular: Repository + _plural: Repositories + what: What's in this Repository? + details: Repository Details + part: Part of the + + context: Found in + bulk: + _singular: "Majority of material found in %{dates}" + _plural: "Majority of material found within %{dates}" resource: _singular: Collection _plural: Collections _public: + extent: Extent + dates: Dates + lang: Language + physdesc: Physical Description + storage: Container Information + access: Conditions Governing Access + use: Conditions Governing Use + additional: Additional Description section: components: Components summary: Summary messages: no_components: Resource has no components + finding_aid: + head: Finding Aid & Administrative Information + title: Title + subtitle: Subtitle + status: Status + author: Author + date: Date + description_rules: Description rules + language: Language of description + sponsor: Sponsor + edition_statement: Edition statement + series_statement: Series statement + note: Note + revision: Revision Statements + related_accessions: + accessions: Accession Records + deaccessions: Deaccession Records + + top_container: + _singular: Container + _plural: Containers related_resource: _singular: Related Collection _plural: Related Collections digital_object: &digital_object_attributes + _singular: Digital Record + _plural: Digital Material _public: + thumbnail: thumbnail + link: Link to digital object section: components: Components summary: Summary @@ -151,12 +385,60 @@ en: messages: no_components: Digital Object Component has no components + subject: + _singular: Subject + _plural: Subjects + used_within: Used within + + component: + _singular: Component + _plural: Components + + unprocessed: Unprocessed Material + + classification: + _singular: Record Group + _plural: Record Groups + creator: Creator + + classification_term: + _singular: Record Group Term + _plural: Record Group Terms + + all_agents: Content Creators + + an_agent: Content Creator + + pui_agent: + _singular: Content Creator + _plural: Content Creators + related: Related Content Creator + record: + _singular: Record + _plural: Records + + record_group: + _singular: Record Group + _plural: Record Groups + agent: - _singular: Name - _plural: Names + _singular: Content Creator + _plural: Content Creators _public: section: summary: Summary + + agent_person: + _singular: Person + + agent_family: + _singular: Family + + agent_corporate_entity: + _singular: Organization + + agent_software: + _singular: Software components: expand: Expand @@ -169,3 +451,1519 @@ en: record_tree: record_tree_tab: Record Tree search_tab: Search Components + + location: Location + + contact: Contact + + hours: Hours + + number_of: "Number of %{type}:" + + parent_inst: Parent Institution + + email: Send email + + fax: Fax + +# The following are the enums for things like notes type, etc. + + enumerations: + linked_event_archival_record_roles: + source: Source + outcome: Outcome + transfer: Transfer + linked_agent_archival_record_relators: + abr: Abridger + act: Actor + adi: Art director + adp: Adapter + anl: Analyst + anm: Animator + ann: Annotator + ape: Appellee + apl: Appellant + app: Applicant + arc: Architect + arr: Arranger + acp: Art copyist + art: Artist + ard: Artistic director + asg: Assignee + asn: Associated name + att: Attributed name + auc: Auctioneer + aut: Author + aqt: Author in quotations or text abstracts + aft: Author of afterword, colophon, etc. + aud: Author of dialog + aui: Author of introduction, etc. + aus: Author of screenplay, etc. + ato: Autographer + ant: Bibliographic antecedent + bnd: Binder + bdd: Binding designer + blw: Blurb writer + bkd: Book designer + bkp: Book producer + bjd: Bookjacket designer + bpd: Bookplate designer + bsl: Bookseller + brl: Braille embosser + brd: Broadcaster + cll: Calligrapher + ctg: Cartographer + cas: Caster + cns: Censor + chr: Choreographer + cng: Cinematographer + cli: Client + cor: Collection registrar + clb: Collaborator + col: Collector + clt: Collotyper + clr: Colorist + cmm: Commentator + cwt: Commentator for written text + com: Compiler + cpl: Complainant + cpt: Complainant-appellant + cpe: Complainant-appellee + cmp: Composer + cmt: Compositor + ccp: Conceptor + cnd: Conductor + con: Conservator + csl: Consultant + csp: Consultant to a project + cos: Contestant + cot: Contestant-appellant + coe: Contestant-appellee + cts: Contestee + ctt: Contestee-appellant + cte: Contestee-appellee + ctr: Contractor + ctb: Contributor + cpc: Copyright claimant + cph: Copyright holder + crr: Corrector + crp: Correspondent + cst: Costume designer + cou: Court governed + crt: Court reporter + cov: Cover designer + cre: Creator + cur: Curator of an exhibition + dnc: Dancer + dtc: Data contributor + dtm: Data manager + dte: Dedicatee + dto: Dedicator + dfd: Defendant + dft: Defendant-appellant + dfe: Defendant-appellee + dgg: Degree grantor + dgs: Degree supervisor + dln: Delineator + dpc: Depicted + dpt: Depositor + dsr: Designer + drt: Director + dis: Dissertant + dbp: Distribution place + dst: Distributor + dnr: Donor + drm: Draftsman + dub: Dubious author + edt: Editor + edc: Editor of compilation + edm: Editor of moving image work + elg: Electrician + elt: Electrotyper + enj: Enacting jurisdiction + eng: Engineer + egr: Engraver + etr: Etcher + evp: Event place + exp: Appraiser + fac: Facsimilist + fld: Field director + fmd: Film director + fds: Film distributor + flm: Film editor + fmp: Film producer + fmk: Filmmaker + fpy: First party + frg: Forger + fmo: Former owner + fnd: Funder + gis: Geographic information specialist + grt: Graphic technician + hnr: Honoree + hst: Host + his: Host institution + ilu: Illuminator + ill: Illustrator + ins: Inscriber + itr: Instrumentalist + ive: Interviewee + ivr: Interviewer + inv: Inventor + isb: Issuing body + jud: Judge + jug: Jurisdiction governed + lbr: Laboratory + ldr: Laboratory director + lsa: Landscape architect + led: Lead + len: Lender + lil: Libelant + lit: Libelant-appellant + lie: Libelant-appellee + lel: Libelee + let: Libelee-appellant + lee: Libelee-appellee + lbt: Librettist + lse: Licensee + lso: Licensor + lgd: Lighting designer + ltg: Lithographer + lyr: Lyricist + mfp: Manufacture place + mfr: Manufacturer + mrb: Marbler + mrk: Markup editor + med: Medium + mdc: Metadata contact + mte: Metal-engraver + mtk: Minute taker + mod: Moderator + mon: Monitor + mcp: Music copyist + msd: Musical director + mus: Musician + nrt: Narrator + osp: Onscreen presenter + opn: Opponent + orm: Organizer of meeting + org: Originator + oth: Other + own: Owner + pan: Panelist + ppm: Papermaker + pta: Patent applicant + pth: Patentee + pat: Patron + prf: Performer + pma: Permitting agency + pht: Photographer + ptf: Plaintiff + ptt: Plaintiff-appellant + pte: Plaintiff-appellee + plt: Platemaker + pra: Praeses + pre: Presenter + prt: Printer + pop: Printer of plates + prm: Printmaker + prc: Process contact + pro: Producer + prn: Production company + pmn: Production manager + prs: Production designer + prd: Production personnel + prp: Production place + prg: Programmer + pdr: Project director + pfr: Proofreader + prv: Provider + pup: Publication place + pbl: Publisher + pbd: Publishing director + ppt: Puppeteer + rdd: Radio director + rpc: Radio producer + rcp: Recipient + rce: Recording engineer + rcd: Recordist + red: Redaktor + ren: Renderer + rpt: Reporter + rps: Repository + rth: Research team head + rtm: Research team member + res: Researcher + rsp: Respondent + rst: Respondent-appellant + rse: Respondent-appellee + rpy: Responsible party + rsg: Restager + rsr: Restorationist + rev: Reviewer + rbr: Rubricator + sce: Scenarist + sad: Scientific advisor + scr: Scribe + scl: Sculptor + spy: Second party + sec: Secretary + sll: Seller + std: Set designer + stg: Setting + sgn: Signer + sng: Singer + sds: Sound designer + spk: Speaker + spn: Sponsor + sgd: Stage director + stm: Stage manager + stn: Standards body + str: Stereotyper + stl: Storyteller + sht: Supporting host + srv: Surveyor + tch: Teacher + tcd: Technical director + tld: Television director + tlp: Television producer + ths: Thesis advisor + trc: Transcriber + trl: Translator + tyd: Type designer + tyg: Typographer + uvp: University place + vdg: Videographer + vac: Voice actor + voc: Vocalist + wit: Witness + wde: Wood engraver + wdc: Woodcutter + wam: Writer of accompanying material + wac: Writer of added commentary + wal: Writer of added lyrics + wat: Writer of added text + win: Writer of introduction + wpr: Writer of preface + wst: Writer of supplementary textual content + linked_agent_event_roles: + authorizer: Authorizer + executing_program: Executing Program + implementer: Implementer + recipient: Recipient + transmitter: Transmitter + validator: Validator + name_source: + cash: Canadian Subject Headings + ingest: Unspecified ingested source + lcshac: Library of Congress Children's Subject Headings + local: Local sources + naf: NACO Authority File + nad: NAD / ARK II Name Authority Database + nal: National Agricultural Library subject headings + rvm: Répertoire de vedettes-matière + ulan: Union List of Artist Names + name_rule: + local: Local rules + aacr: Anglo-American Cataloging Rules + dacs: "Describing Archives: A Content Standard" + rda: Resource Description and Access + extent_extent_type: + cassettes: Cassettes + cubic_feet: Cubic Feet + files: Files + gigabytes: Gigabytes + leaves: Leaves + linear_feet: Linear Feet + megabytes: Megabytes + photographic_prints: Photographic Prints + photographic_slides: Photographic Slides + reels: Reels + sheets: Sheets + terabytes: Terabytes + volumes: Volumes + collection_management_processing_priority: + none: Not specified + high: High + medium: Medium + low: Low + collection_management_processing_status: + none: Not specified + new: New + in_progress: In Progress + completed: Completed + date_era: + ce: ce + date_calendar: + gregorian: Gregorian + digital_object_digital_object_type: + cartographic: Cartographic + mixed_materials: Mixed Materials + moving_image: Moving Image + notated_music: Notated Music + software_multimedia: Software, Multimedia + sound_recording: Sound Recording + sound_recording_musical: Sound Recording (Musical) + sound_recording_nonmusical: Sound Recording (Non-musical) + still_image: Still Image + text: Text + digital_object_level: + collection: Collection + work: Work + image: Image + event_event_type: + accession: Accession + accumulation: Accumulation + acknowledgement: Acknowledgement + acknowledgement_sent: Acknowledgement Sent + acknowledgement_received: Acknowledgement Received + agreement_received: Agreement Received + agreement_sent: Agreement Sent + agreement_signed: Agreement Signed + appraisal: Appraisal + assessment: Assessment + capture: Capture + cataloged: Cataloged + collection: Collection + compression: Compression + contribution: Contribution + component_transfer: Component Transfer + copyright_transfer: Copyright Transfer + custody_transfer: Custody Transfer + deaccession: Deaccession + decompression: Decompression + decryption: Decryption + deletion: Deletion + digital_signature_validation: Digital Signature Validation + fixity_check: Fixity Check + ingestion: Ingestion + message_digest_calculation: Message Digest Calculation + migration: Migration + normalization: Normalization + processed: Processed + processing_new: Processing New + processing_started: Processing Started + processing_in_progress: Processing in Progress + processing_completed: Processing Completed + publication: Publication + replication: Replication + rights_transferred: Rights Transferred + validation: Validation + virus_check: Virus Check + resource_resource_type: + collection: Collection + publications: Publications + papers: Papers + records: Records + resource_finding_aid_description_rules: + aacr: Anglo-American Cataloguing Rules + cco: Cataloging Cultural Objects + dacs: "Describing Archives: A Content Standard" + rad: Rules for Archival Description + isadg: International Standard for Archival Description - General + resource_finding_aid_status: + completed: Completed + in_progress: In Progress + under_revision: Under Revision + unprocessed: Unprocessed + instance_instance_type: + accession: Accession + audio: Audio + books: Books + computer_disks: Computer Disks + digital_object: Digital Object + digital_object_link: Digital Object Link + graphic_materials: Graphic Materials + maps: Maps + microform: Microform + mixed_materials: Mixed Materials + moving_images: Moving Images + realia: Realia + text: Text + accession_acquisition_type: + deposit: Deposit + gift: Gift + purchase: Purchase + transfer: Transfer + accession_resource_type: + collection: Collection + publications: Publications + papers: Papers + records: Records + subject_source: + aat: "Art & Architecture Thesaurus" + cash: Canadian Subject Headings + gmgpc: "TGM II, Genre and physical characteristic terms" + ingest: Unspecified ingested source + lcsh: Library of Congress Subject Headings + lcshac: Library of Congress Children's Subject Headings + local: Local sources + mesh: Medical Subject Headings + nal: National Agricultural Library subject headings + rbgenr: "Genre Terms: A Thesaurus for Use in Rare Book and Special Collections Cataloguing" + rvm: Répertoire de vedettes-matière + tgn: Getty Thesaurus of Geographic Names + event_outcome: + pass: Pass + partial pass: Partial Pass + fail: Fail + agent_contact_salutation: + mr: Mr. + mrs: Mrs. + ms: Ms. + madame: Madame + sir: Sir + container_type: + box: Box + carton: Carton + case: Case + folder: Folder + frame: Frame + object: Object + page: Page + reel: Reel + volume: Volume + file_version_use_statement: + audio-clip: Audio-Clip + audio-master: Audio-Master + audio-master-edited: Audio-Master-Edited + audio-service: Audio-Service + audio-streaming: Audio-Streaming + image-master: Image-Master + image-master-edited: Image-Master-Edited + image-service: Image-Service + image-service-edited: Image-Service-Edited + image-thumbnail: Image-Thumbnail + text-codebook: Text-Codebook + text-data: Text-Data + text-data_definition: Text-Data Definition + text-georeference: Text-Georeference + text-ocr-edited: Text-Ocr-Edited + text-ocr-unedited: Text-Ocr-Unedited + text-tei-transcripted: Text-Tei-Transcripted + text-tei-translated: Text-Tei-Translated + video-clip: Video-Clip + video-master: Video-Master + video-master-edited: Video-Master-Edited + video-service: Video-Service + video-streaming: Video-Streaming + file_version_checksum_methods: + md5: MD5 + sha-1: SHA-1 + sha-256: SHA-256 + sha-384: SHA-384 + sha-512: SHA-512 + language_iso639_2: + aar: Afar + abk: Abkhazian + ace: Achinese + ach: Acoli + ada: Adangme + ady: Adyghe; Adygei + afa: Afro-Asiatic languages + afh: Afrihili + afr: Afrikaans + ain: Ainu + aka: Akan + akk: Akkadian + alb: Albanian + ale: Aleut + alg: Algonquian languages + alt: Southern Altai + amh: Amharic + ang: "English, Old (ca.450-1100)" + anp: Angika + apa: Apache languages + ara: Arabic + arc: Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE) + arg: Aragonese + arm: Armenian + arn: Mapudungun; Mapuche + arp: Arapaho + art: Artificial languages + arw: Arawak + asm: Assamese + ast: Asturian; Bable; Leonese; Asturleonese + ath: Athapascan languages + aus: Australian languages + ava: Avaric + ave: Avestan + awa: Awadhi + aym: Aymara + aze: Azerbaijani + bad: Banda languages + bai: Bamileke languages + bak: Bashkir + bal: Baluchi + bam: Bambara + ban: Balinese + baq: Basque + bas: Basa + bat: Baltic languages + bej: Beja; Bedawiyet + bel: Belarusian + bem: Bemba + ben: Bengali + ber: Berber languages + bho: Bhojpuri + bih: Bihari languages + bik: Bikol + bin: Bini; Edo + bis: Bislama + bla: Siksika + bnt: Bantu (Other) + bos: Bosnian + bra: Braj + bre: Breton + btk: Batak languages + bua: Buriat + bug: Buginese + bul: Bulgarian + bur: Burmese + byn: Blin; Bilin + cad: Caddo + cai: Central American Indian languages + car: Galibi Carib + cat: Catalan; Valencian + cau: Caucasian languages + ceb: Cebuano + cel: Celtic languages + cha: Chamorro + chb: Chibcha + che: Chechen + chg: Chagatai + chi: Chinese + chk: Chuukese + chm: Mari + chn: Chinook jargon + cho: Choctaw + chp: Chipewyan; Dene Suline + chr: Cherokee + chu: Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic + chv: Chuvash + chy: Cheyenne + cmc: Chamic languages + cop: Coptic + cor: Cornish + cos: Corsican + cpe: "Creoles and pidgins, English-based" + cpf: "Creoles and pidgins, French-based" + cpp: "Creoles and pidgins, Portuguese-based" + cre: Cree + crh: Crimean Tatar; Crimean Turkish + crp: Creoles and pidgins + csb: Kashubian + cus: Cushitic languages + cze: Czech + dak: Dakota + dan: Danish + dar: Dargwa + day: Land Dayak languages + del: Delaware + den: Slave (Athapascan) + dgr: Dogrib + din: Dinka + div: Divehi; Dhivehi; Maldivian + doi: Dogri + dra: Dravidian languages + dsb: Lower Sorbian + dua: Duala + dum: "Dutch, Middle (ca.1050-1350)" + dut: Dutch; Flemish + dyu: Dyula + dzo: Dzongkha + efi: Efik + egy: Egyptian (Ancient) + eka: Ekajuk + elx: Elamite + eng: English + enm: "English, Middle (1100-1500)" + epo: Esperanto + est: Estonian + ewe: Ewe + ewo: Ewondo + fan: Fang + fao: Faroese + fat: Fanti + fij: Fijian + fil: Filipino; Pilipino + fin: Finnish + fiu: Finno-Ugrian languages + fon: Fon + fre: French + frm: "French, Middle (ca.1400-1600)" + fro: "French, Old (842-ca.1400)" + frr: Northern Frisian + frs: Eastern Frisian + fry: Western Frisian + ful: Fulah + fur: Friulian + gaa: Ga + gay: Gayo + gba: Gbaya + gem: Germanic languages + geo: Georgian + ger: German + gez: Geez + gil: Gilbertese + gla: Gaelic; Scottish Gaelic + gle: Irish + glg: Galician + glv: Manx + gmh: "German, Middle High (ca.1050-1500)" + goh: "German, Old High (ca.750-1050)" + gon: Gondi + gor: Gorontalo + got: Gothic + grb: Grebo + grc: "Greek, Ancient (to 1453)" + gre: "Greek, Modern (1453-)" + grn: Guarani + gsw: Swiss German; Alemannic; Alsatian + guj: Gujarati + gwi: Gwich'in + hai: Haida + hat: Haitian; Haitian Creole + hau: Hausa + haw: Hawaiian + heb: Hebrew + her: Herero + hil: Hiligaynon + him: Himachali languages; Western Pahari languages + hin: Hindi + hit: Hittite + hmn: Hmong; Mong + hmo: Hiri Motu + hrv: Croatian + hsb: Upper Sorbian + hun: Hungarian + hup: Hupa + iba: Iban + ibo: Igbo + ice: Icelandic + ido: Ido + iii: Sichuan Yi; Nuosu + ijo: Ijo languages + iku: Inuktitut + ile: Interlingue; Occidental + ilo: Iloko + ina: Interlingua (International Auxiliary Language Association) + inc: Indic languages + ind: Indonesian + ine: Indo-European languages + inh: Ingush + ipk: Inupiaq + ira: Iranian languages + iro: Iroquoian languages + ita: Italian + jav: Javanese + jbo: Lojban + jpn: Japanese + jpr: Judeo-Persian + jrb: Judeo-Arabic + kaa: Kara-Kalpak + kab: Kabyle + kac: Kachin; Jingpho + kal: Kalaallisut; Greenlandic + kam: Kamba + kan: Kannada + kar: Karen languages + kas: Kashmiri + kau: Kanuri + kaw: Kawi + kaz: Kazakh + kbd: Kabardian + kha: Khasi + khi: Khoisan languages + khm: Central Khmer + kho: Khotanese; Sakan + kik: Kikuyu; Gikuyu + kin: Kinyarwanda + kir: Kirghiz; Kyrgyz + kmb: Kimbundu + kok: Konkani + kom: Komi + kon: Kongo + kor: Korean + kos: Kosraean + kpe: Kpelle + krc: Karachay-Balkar + krl: Karelian + kro: Kru languages + kru: Kurukh + kua: Kuanyama; Kwanyama + kum: Kumyk + kur: Kurdish + kut: Kutenai + lad: Ladino + lah: Lahnda + lam: Lamba + lao: Lao + lat: Latin + lav: Latvian + lez: Lezghian + lim: Limburgan; Limburger; Limburgish + lin: Lingala + lit: Lithuanian + lol: Mongo + loz: Lozi + ltz: Luxembourgish; Letzeburgesch + lua: Luba-Lulua + lub: Luba-Katanga + lug: Ganda + lui: Luiseno + lun: Lunda + luo: Luo (Kenya and Tanzania) + lus: Lushai + mac: Macedonian + mad: Madurese + mag: Magahi + mah: Marshallese + mai: Maithili + mak: Makasar + mal: Malayalam + man: Mandingo + mao: Maori + map: Austronesian languages + mar: Marathi + mas: Masai + may: Malay + mdf: Moksha + mdr: Mandar + men: Mende + mga: "Irish, Middle (900-1200)" + mic: Mi'kmaq; Micmac + min: Minangkabau + mis: Uncoded languages + mkh: Mon-Khmer languages + mlg: Malagasy + mlt: Maltese + mnc: Manchu + mni: Manipuri + mno: Manobo languages + moh: Mohawk + mon: Mongolian + mos: Mossi + mul: Multiple languages + mun: Munda languages + mus: Creek + mwl: Mirandese + mwr: Marwari + myn: Mayan languages + myv: Erzya + nah: Nahuatl languages + nai: North American Indian languages + nap: Neapolitan + nau: Nauru + nav: Navajo; Navaho + nbl: "Ndebele, South; South Ndebele" + nde: "Ndebele, North; North Ndebele" + ndo: Ndonga + nds: "Low German; Low Saxon; German, Low; Saxon, Low" + nep: Nepali + new: Nepal Bhasa; Newari + nia: Nias + nic: Niger-Kordofanian languages + niu: Niuean + nno: "Norwegian Nynorsk; Nynorsk, Norwegian" + nob: "Norwegian Bokmål; Bokmål, Norwegian" + nog: Nogai + non: "Norse, Old" + nor: Norwegian + nqo: N'Ko + nso: Pedi; Sepedi; Northern Sotho + nub: Nubian languages + nwc: Classical Newari; Old Newari; Classical Nepal Bhasa + nya: Chichewa; Chewa; Nyanja + nym: Nyamwezi + nyn: Nyankole + nyo: Nyoro + nzi: Nzima + oci: Occitan (post 1500); Provençal + oji: Ojibwa + ori: Oriya + orm: Oromo + osa: Osage + oss: Ossetian; Ossetic + ota: "Turkish, Ottoman (1500-1928)" + oto: Otomian languages + paa: Papuan languages + pag: Pangasinan + pal: Pahlavi + pam: Pampanga; Kapampangan + pan: Panjabi; Punjabi + pap: Papiamento + pau: Palauan + peo: "Persian, Old (ca.600-400 B.C.)" + per: Persian + phi: Philippine languages + phn: Phoenician + pli: Pali + pol: Polish + pon: Pohnpeian + por: Portuguese + pra: Prakrit languages + pro: "Provençal, Old (to 1500)" + pus: Pushto; Pashto + qaa-qtz: "Reserved for local use" + que: Quechua + raj: Rajasthani + rap: Rapanui + rar: Rarotongan; Cook Islands Maori + roa: Romance languages + roh: Romansh + rom: Romany + rum: Romanian; Moldavian; Moldovan + run: Rundi + rup: Aromanian; Arumanian; Macedo-Romanian + rus: Russian + sad: Sandawe + sag: Sango + sah: Yakut + sai: South American Indian (Other) + sal: Salishan languages + sam: Samaritan Aramaic + san: Sanskrit + sas: Sasak + sat: Santali + scn: Sicilian + sco: Scots + sel: Selkup + sem: Semitic languages + sga: "Irish, Old (to 900)" + sgn: Sign Languages + shn: Shan + sid: Sidamo + sin: Sinhala; Sinhalese + sio: Siouan languages + sit: Sino-Tibetan languages + sla: Slavic languages + slo: Slovak + slv: Slovenian + sma: Southern Sami + sme: Northern Sami + smi: Sami languages + smj: Lule Sami + smn: Inari Sami + smo: Samoan + sms: Skolt Sami + sna: Shona + snd: Sindhi + snk: Soninke + sog: Sogdian + som: Somali + son: Songhai languages + sot: "Sotho, Southern" + spa: Spanish; Castilian + srd: Sardinian + srn: Sranan Tongo + srp: Serbian + srr: Serer + ssa: Nilo-Saharan languages + ssw: Swati + suk: Sukuma + sun: Sundanese + sus: Susu + sux: Sumerian + swa: Swahili + swe: Swedish + syc: Classical Syriac + syr: Syriac + tah: Tahitian + tai: Tai languages + tam: Tamil + tat: Tatar + tel: Telugu + tem: Timne + ter: Tereno + tet: Tetum + tgk: Tajik + tgl: Tagalog + tha: Thai + tib: Tibetan + tig: Tigre + tir: Tigrinya + tiv: Tiv + tkl: Tokelau + tlh: Klingon; tlhIngan-Hol + tli: Tlingit + tmh: Tamashek + tog: Tonga (Nyasa) + ton: Tonga (Tonga Islands) + tpi: Tok Pisin + tsi: Tsimshian + tsn: Tswana + tso: Tsonga + tuk: Turkmen + tum: Tumbuka + tup: Tupi languages + tur: Turkish + tut: Altaic languages + tvl: Tuvalu + twi: Twi + tyv: Tuvinian + udm: Udmurt + uga: Ugaritic + uig: Uighur; Uyghur + ukr: Ukrainian + umb: Umbundu + und: Undetermined + urd: Urdu + uzb: Uzbek + vai: Vai + ven: Venda + vie: Vietnamese + vol: Volapük + vot: Votic + wak: Wakashan languages + wal: Walamo + war: Waray + was: Washo + wel: Welsh + wen: Sorbian languages + wln: Walloon + wol: Wolof + xal: Kalmyk; Oirat + xho: Xhosa + yao: Yao + yap: Yapese + yid: Yiddish + yor: Yoruba + ypk: Yupik languages + zap: Zapotec + zbl: Blissymbols; Blissymbolics; Bliss + zen: Zenaga + zha: Zhuang; Chuang + znd: Zande languages + zul: Zulu + zun: Zuni + zxx: No linguistic content; Not applicable + zza: Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki + archival_record_level: + class: Class + collection: Collection + file: File + fonds: Fonds + item: Item + otherlevel: Other Level + recordgrp: Record Group + series: Series + subfonds: Sub-Fonds + subgrp: Sub-Group + subseries: Sub-Series + container_location_status: + current: Current + previous: Previous + date_type: + expression: Expression + single: Single + bulk: Bulk Dates + inclusive: Inclusive Dates + range: Range + date_label: + record_keeping: Record Keeping + broadcast: Broadcast + copyright: Copyright + creation: Creation + deaccession: Deaccession + agent_relation: Agent Relation + digitized: Digitized + existence: Existence + event: Event + issued: Issued + modified: Modified + publication: Publication + usage: Usage + other: Other + date_certainty: + approximate: Approximate + inferred: Inferred + questionable: Questionable + extent_portion: + whole: Whole + part: Part + deaccession_scope: + whole: Whole + part: Part + file_version_xlink_actuate_attribute: + none: none + other: other + onLoad: onLoad + onRequest: onRequest + file_version_xlink_show_attribute: + new: new + replace: replace + embed: embed + other: other + none: none + file_version_file_format_name: + aiff: Audio Interchange File Format + avi: Audio/Video Interleaved Format + gif: Graphics Interchange Format + jpeg: JPEG File Interchange Format + mp3: MPEG Audio Layer 3 + pdf: Portable Document Format + tiff: Tagged Image File Format + txt: Plain Text File + location_temporary: + conservation: Conservation + exhibit: Exhibit + loan: Loan + reading_room: Reading Room + name_person_name_order: + direct: Direct + inverted: Indirect + country_iso_3166: + AF: Afghanistan + AX: Åland Islands + AL: Albania + DZ: Algeria + AS: American Samoa + AD: Andorra + AO: Angola + AI: Anguilla + AQ: Antarctica + AG: Antigua and Barbuda + AR: Argentina + AM: Armenia + AW: Aruba + AU: Australia + AT: Austria + AZ: Azerbaijan + BS: Bahamas + BH: Bahrain + BD: Bangladesh + BB: Barbados + BY: Belarus + BE: Belgium + BZ: Belize + BJ: Benin + BM: Bermuda + BT: Bhutan + BO: Bolivia + BQ: "Bonaire, Sint Eustatius and Saba" + BA: Bosnia and Herzegovina + BW: Botswana + BV: Bouvet Island + BR: Brazil + IO: British Indian Ocean Territory + BN: Brunei Darussalam + BG: Bulgaria + BF: Burkina Faso + BI: Burundi + KH: Cambodia + CM: Cameroon + CA: Canada + CV: Cape Verde + KY: Cayman Islands + CF: Central African Republic + TD: Chad + CL: Chile + CN: China + CX: Christmas island + CC: Cocos (Keeling) Islands + CO: Colombia + KM: Comoros + CG: Congo + CD: Democratic Republic of the Congo + CK: Cook Islands + CR: Costa Rica + CI: Côte d'Ivoire + HR: Croatia + CU: Cuba + CW: Curaçao + CY: Cyprus + CZ: Czech Republic + DK: Denmark + DJ: Djibouti + DM: Dominica + DO: Dominican Republic + EC: Ecuador + EG: Egypt + SV: El Salvador + GQ: Equatorial Guinea + ER: Eritrea + EE: Estonia + ET: Ethiopia + FK: Falkland Islands (Malvinas) + FO: Faroe Islands + FJ: Fiji + FI: Finland + FR: France + GF: French Guiana + PF: French Polynesia + TF: French Southern Territories + GA: Gabon + GM: Gambia + GE: Georgia + DE: Germany + GH: Ghana + GI: Gibraltar + GR: Greece + GL: Greenland + GD: Grenada + GP: Guadeloupe + GU: Guam + GT: Guatemala + GG: Guernsey + GN: Guinea + GW: Guinea-Bissau + GY: Guyana + HT: Haiti + HM: Heard Island and Mcdonald Islands + VA: Holy See (Vatican City State) + HN: Honduras + HK: Hong Kong + HU: Hungary + IS: Iceland + IN: India + ID: Indonesia + IR: "Iran, Islamic Republic of" + IQ: Iraq + IE: Ireland + IM: Isle of Man + IL: Israel + IT: Italy + JM: Jamaica + JP: Japan + JE: Jersey + JO: Jordan + KZ: Kazakhstan + KE: Kenya + KI: Kiribati + KP: "Korea, Democratic People's Republic of" + KR: "Korea, Republic of" + KW: Kuwait + KG: Kyrgyzstan + LA: "Lao People's Democratic Republic" + LV: Latvia + LB: Lebanon + LS: Lesotho + LR: Liberia + LY: Libya + LI: Liechtenstein + LT: Lithuania + LU: Luxembourg + MO: Macao + MK: "Macedonia, Former Yugoslav Republic of" + MG: Madagascar + MW: Malawi + MY: Malaysia + MV: Maldives + ML: Mali + MT: Malta + MH: Marshall Islands + MQ: Martinique + MR: Mauritania + MU: Mauritius + YT: Mayotte + MX: Mexico + FM: "Micronesia, Federated States of" + MD: "Moldova, Republic of" + MC: Monaco + MN: Mongolia + ME: Montenegro + MS: Montserrat + MA: Morocco + MZ: Mozambique + MM: Myanmar + NA: Namibia + NR: Nauru + NP: Nepal + NL: Netherlands + NC: New Caledonia + NZ: New Zealand + NI: Nicaragua + NE: Niger + NG: Nigeria + NU: Niue + NF: Norfolk Island + MP: Northern Mariana Islands + NO: Norway + OM: Oman + PK: Pakistan + PW: Palau + PS: "Palestinian Territory, Occupied" + PA: Panama + PG: Papua New Guinea + PY: Paraguay + PE: Peru + PH: Philippines + PN: Pitcairn + PL: Poland + PT: Portugal + PR: Puerto rico + QA: Qatar + RE: Réunion + RO: Romania + RU: Russian Federation + RW: Rwanda + BL: Saint Barthélemy + SH: "Saint Helena, Ascension and Tristan da Cunha" + KN: Saint Kitts and Nevis + LC: Saint Lucia + MF: Saint Martin (French Part) + PM: Saint Pierre and Miquelon + VC: Saint Vincent and the Grenadines + WS: Samoa + SM: San Marino + ST: Sao Tome and Principe + SA: Saudi Arabia + SN: Senegal + RS: Serbia + SC: Seychelles + SL: Sierra Leone + SG: Singapore + SX: Sint Maarten (Dutch Part) + SK: Slovakia + SI: Slovenia + SB: Solomon Islands + SO: Somalia + ZA: South Africa + GS: South Georgia and the South Sandwich Islands + SS: South Sudan + ES: Spain + LK: Sri Lanka + SD: Sudan + SR: Suriname + SJ: Svalbard and Jan Mayen + SZ: Swaziland + SE: Sweden + CH: Switzerland + SY: Syrian Arab Republic + TW: "Taiwan, Province of China" + TJ: Tajikistan + TZ: "Tanzania, United Republic of" + TH: Thailand + TL: Timor-Leste + TG: Togo + TK: Tokelau + TO: Tonga + TT: Trinidad and Tobago + TN: Tunisia + TR: Turkey + TM: Turkmenistan + TC: Turks and Caicos Islands + TV: Tuvalu + UG: Uganda + UA: Ukraine + AE: United Arab Emirates + GB: United Kingdom + US: United States + UM: United States Minor Outlying Islands + UY: Uruguay + UZ: Uzbekistan + VU: Vanuatu + VE: Venezuela + VN: Viet Nam + VG: "Virgin Islands, British" + VI: "Virgin Islands, U.S." + WF: Wallis and Futuna + EH: Western Sahara + YE: Yemen + ZM: Zambia + ZW: Zimbabwe + rights_statement_rights_type: + intellectual_property: Intellectual Property + license: License + statute: Statute + institutional_policy: Institutional Policy + rights_statement_ip_status: + copyrighted: Copyrighted + public_domain: Public Domain + unknown: Unknown + subject_term_type: + cultural_context: Cultural context + function: Function + geographic: Geographic + genre_form: "Genre / Form" + occupation: Occupation + style_period: "Style / Period" + technique: Technique + temporal: Temporal + topical: Topical + uniform_title: Uniform Title + _note_types: ¬e_type_definitions + accessrestrict: Conditions Governing Access + accruals: Accruals + acqinfo: Immediate Source of Acquisition + altformavail: Existence and Location of Copies + appraisal: Appraisal + arrangement: Arrangement + bibliography: Bibliography + bioghist: Biographical / Historical + custodhist: Custodial History + fileplan: File Plan + index: Index + odd: General + otherfindaid: Other Finding Aids + originalsloc: Existence and Location of Originals + phystech: Physical Characteristics and Technical Requirements + prefercite: Preferred Citation + processinfo: Processing Information + relatedmaterial: Related Archival Materials + scopecontent: Scope and Contents + separatedmaterial: Separated Materials + userestrict: Conditions Governing Use + dimensions: Dimensions + legalstatus: Legal Status + summary: Summary + edition: Edition + extent: Extent + note: General Note + inscription: Inscription + langmaterial: Language of Materials + physdesc: Physical Description + relatedmaterial: Related Materials + abstract: Overview + physloc: Physical Location + materialspec: Materials Specific Details + physfacet: Physical Facet + note_digital_object_type: + <<: *note_type_definitions + note_multipart_type: + <<: *note_type_definitions + note_singlepart_type: + <<: *note_type_definitions + linked_agent_role: + creator: Creator + source: Source + subject: Subject + agent_relationship_associative_relator: + is_associative_with: Associative with Related + agent_relationship_earlierlater_relator: + is_earlier_form_of: Earlier Form of Related + is_later_form_of: Later Form of Related + agent_relationship_parentchild_relator: + is_child_of: Child of Related + is_parent_of: Parent of Related + accession_sibling_relator: + sibling_of: Is Sibling of + accession_parts_relator: + forms_part_of: Forms Part of + has_part: Has Part + accession_parts_relator_type: + part: "\"Part\" relationship" + accession_sibling_relator_type: + bound_with: "\"Bound With\" relationship" + agent_relationship_subordinatesuperior_relator: + is_subordinate_to: Is Subordinate to Related + is_superior_of: Is Superior of Related + note_index_item_type: + name: Name + person: Person + family: Family + corporate_entity: Corporate Entity + subject: Subject + function: Function + occupation: Occupation + title: Title + geographic_name: Geographic Name + user_defined_enum_1: + novalue: No value defined + user_defined_enum_2: + novalue: No value defined + user_defined_enum_3: + novalue: No value defined + user_defined_enum_4: + novalue: No value defined + job_type: + print_to_pdf_job: Print To PDF + import_job: Import Data + find_and_replace_job: Batch Find and Replace (Beta) + report_job: Reports + container_conversion_job: Reports + dimension_units: + inches: Inches + feet: Feet + yards: Yards + millimeters: Millimeters + centimeters: Centimeters + meters: Meters + restriction_type: + RestrictedSpecColl: 1 - Donor/university imposed access restriction + RestrictedCurApprSpecColl: 2 - Repository imposed access restriction + RestrictedFragileSpecColl: 3 - Restricted fragile + InProcessSpecColl: 4 - Restricted in-process + ColdStorageBrbl: 5 - Other + location_function_type: + av_materials: Audiovisual Materials + arrivals: Arrivals + shared: Shared + + + enumeration_names: + accession_acquisition_type: Accession Acquisition Type + accession_parts_relator: Accession Parts Relator + accession_parts_relator_type: Accession Parts Relator Type + accession_sibling_relator: "Accession Sibling Relator" + accession_sibling_relator_type: Accession Sibling Relator Type + accession_resource_type: Accession Resource Type + agent_contact_salutation: Agent Contact Salutation + agent_relationship_associative_relator: Agent Relationship Associative Relator + agent_relationship_earlierlater_relator: Agent Relationship Earlierlater Relator + agent_relationship_parentchild_relator: Agent Relationship Parentchild Relator + agent_relationship_subordinatesuperior_relator: Agent Relationship Subordinatesuperior Relator + archival_record_level: Archival Record Level + collection_management_processing_priority: Collection Management Processing Priority + collection_management_processing_status: Collection Management Processing Status + container_location_status: Container Location Status + container_type: Container Type + country_iso_3166: Country ISO 3166 + date_calendar: Date Calendar + date_certainty: Date Certainty + date_era: Date Era + date_label: Date Label + date_type: Date Type + deaccession_scope: Deaccession Scope + digital_object_digital_object_type: Digital Object Digital Object Type + digital_object_level: Digital Object Level + event_event_type: Event Event Type + event_outcome: Event Outcome + extent_extent_type: Extent Extent Type + extent_portion: Extent Portion + file_version_checksum_methods: File Version Checksum Methods + file_version_file_format_name: File Version File Format Name + file_version_use_statement: File Version Use Statement + file_version_xlink_actuate_attribute: File Version Xlink Actuate Attribute + file_version_xlink_show_attribute: File Version Xlink Show Attribute + instance_instance_type: Instance Instance Type + language_iso639_2: Language ISO 639-2 + linked_agent_archival_record_relators: Linked Agent Archival Record Relators + linked_agent_event_roles: Linked Agent Event Roles + linked_agent_role: Linked Agent Role + linked_event_archival_record_roles: Linked Event Archival Record Roles + location_temporary: Location Temporary + name_person_name_order: Name Person Name Order + name_rule: Name Rule + name_source: Name Source + note_bibliography_type: Note Bibliography Type + note_digital_object_type: Note Digital Object Type + note_index_item_type: Note Index Item Type + note_index_type: Note Index Type + note_multipart_type: Note Multipart Type + note_orderedlist_enumeration: Note Orderedlist Enumeration + note_singlepart_type: Note Singlepart Type + telephone_number_type: Telephone Number Type + resource_finding_aid_description_rules: Resource Finding Aid Description Rules + resource_finding_aid_status: Resource Finding Aid Status + resource_resource_type: Resource Resource Type + rights_statement_ip_status: Rights Statement IP Status + rights_statement_rights_type: Rights Statement Rights Type + subject_source: Subject Source + subject_term_type: Subject Term Type + user_defined_enum_1: User Defined Enum 1 + user_defined_enum_2: User Defined Enum 2 + user_defined_enum_3: User Defined Enum 3 + user_defined_enum_4: User Defined Enum 4 + job_type: Job Type + dimension_units: Dimension Units + restriction_type: Local Access Restriction Type + location_function_type: Location Function Type +# added to support things like provenance + inventory: Inventory + disposition: Disposition + content_description: Content Description + provenance: Provenance + access_date: Access Date + access_restrictions_note: Conditions Governing Access + use_restrictions_note: Conditions Governing Use \ No newline at end of file diff --git a/public-new/config/routes.rb b/public-new/config/routes.rb index 51a203fe1a..b9ce8c4e25 100644 --- a/public-new/config/routes.rb +++ b/public-new/config/routes.rb @@ -1,93 +1,80 @@ -ArchivesSpacePublic::Application.routes.draw do - - [AppConfig[:public_proxy_prefix], AppConfig[:public_prefix]].uniq.each do |prefix| - - scope prefix do - - root "site#index" - - match 'api/repositories/:repo_id/resources/:id' => 'records#resource', :via => [:get] - match 'api/repositories/:repo_id/archival_objects/:id' => 'records#archival_object', :via => [:get] - match 'api/repositories/:repo_id/accessions/:id' => 'records#accession', :via => [:get] - match 'api/repositories/:repo_id/digital_objects/:id' => 'records#digital_object', :via => [:get] - match 'api/repositories/:repo_id/classifications/:id/tree' => 'trees#classification', :via => [:get] - - match 'api/repositories/:repo_id/classifications/:id' => 'records#classification', :via => [:get] - match 'api/repositories/:repo_id/classification_terms/:id' => 'records#classification_term', :via => [:get] - - match 'api/people/:id' => 'records#agent_person', :via => [:get] - - match 'api/subjects/:id' => 'records#subject', :via => [:get] - - - match 'api/repositories/:repo_id' => 'records#repository', :via => [:get] - - - match 'api/trees' => 'trees#fetch', :via => [:get] - - match 'api/search' => 'search#search', :via => [:get] - match 'api/advanced_search' => 'search#advanced_search', :via => [:get] - - match 'api/(*url)' => "site#bad_request", :via => [:get] - - get '/(*url)' => "site#index" - - - # The priority is based upon order of creation: first created -> highest priority. - # See how all your routes lay out with "rake routes". - - # You can have the root of your site routed with "root" - # root 'welcome#index' - - # Example of regular route: - # get 'products/:id' => 'catalog#view' - - # Example of named route that can be invoked with purchase_url(id: product.id) - # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase - - # Example resource route (maps HTTP verbs to controller actions automatically): - # resources :products - - # Example resource route with options: - # resources :products do - # member do - # get 'short' - # post 'toggle' - # end - # - # collection do - # get 'sold' - # end - # end - - # Example resource route with sub-resources: - # resources :products do - # resources :comments, :sales - # resource :seller - # end - - # Example resource route with more complex sub-resources: - # resources :products do - # resources :comments - # resources :sales do - # get 'recent', on: :collection - # end - # end - - # Example resource route with concerns: - # concern :toggleable do - # post 'toggle' - # end - # resources :posts, concerns: :toggleable - # resources :photos, concerns: :toggleable - - # Example resource route within a namespace: - # namespace :admin do - # # Directs /admin/products/* to Admin::ProductsController - # # (app/controllers/admin/products_controller.rb) - # resources :products - # end - - end - end +Rails.application.routes.draw do + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html + + get '/', to: 'welcome#show' #'index#index' + get '/welcome', to: 'welcome#show' + post '/cite', to: 'cite#show' + get 'objects/search' => 'objects#search' + post 'objects/search' => 'objects#search' + get 'objects' => 'objects#index' + post 'objects' => 'objects#index' + get 'accessions/search' => 'accessions#search' + post 'accessions/search' => 'accessions#search' + get 'accessions' => 'accessions#index' + post 'accessions' => 'accessions#index' + get 'classifications/search' => 'classifications#search' + post 'classifications/search' => 'classifications#search' + get 'classifications' => 'classifications#index' + post 'classifications' => 'classifications#index' + get 'fill_request' => 'requests#make_request' + post 'fill_request' => 'requests#make_request' + get 'subjects/search' => 'subjects#search' + post 'subjects/search' => 'subjects#search' + get "subjects/:id" => 'subjects#show' + get 'subjects' => 'subjects#index' + post 'subjects' => 'subjects#index' + get 'agents/search' => 'agents#search' + post 'agents/search' => 'agents#search' + get "agents/:eid/:id" => 'agents#show' + get 'agents' => 'agents#index' + + + get "repositories/:rid/top_containers/:id" => 'containers#show' + post "repositories/:rid/top_containers/:id" => 'containers#show' + get 'repositories/resources' => 'resources#index' + get "repositories/:rid/accessions/:id" => 'accessions#show' + post "repositories/:rid/archival_objects/:id/request" => 'objects#request_showing' + get "repositories/:rid/archival_objects/:id/request" => 'objects#request_showing' + get "repositories/:rid/classifications/:id" => 'classifications#show' + get "repositories/:rid/classification_terms/:id" => 'classifications#term' + get "repositories/:repo_id/resources/:id/search" => 'resources#search' + get "repositories/:rid/resources/:id" => 'resources#show' + get "repositories/:rid/resources/:id/inventory" => 'resources#inventory' + get "repositories/:rid/:obj_type/:id" => 'objects#show' + get "repositories/:rid/classifications/" => 'classifications#index' + post "repositories/:rid/classifications/" => 'classifications#index' + get "repositories/:rid/resources" => 'resources#index' + post "repositories/:rid/resources" => 'resources#index' + get "repositories/:rid/search" => 'search#search' + post "repositories/:rid/search" => 'search#search' + get "repositories/:rid/agents" => 'agents#index' + post "repositories/:rid/agents" => 'agents#index' + get "repositories/:rid/subjects" => 'subjects#index' + post "repositories/:rid/subjects" => 'subjects#index' + get "repositories/:rid/objects" => 'objects#index' + post "repositories/:rid/objects" => 'objects#index' + post "repositories/:rid/records" => 'objects#index' + get "repositories/:id" => 'repositories#show' + post "repositories/:id" => 'repositories#show' + + get "repositories/:rid/resources/:id/to-infinity-and-beyond" => 'resources#infinite' + get "repositories/:rid/resources/:id/infinite/waypoints" => 'resources#waypoints' + + get "repositories/:rid/resources/:id/tree/root" => 'resources#tree_root' + get "repositories/:rid/resources/:id/tree/waypoint" => 'resources#tree_waypoint' + get "repositories/:rid/resources/:id/tree/node" => 'resources#tree_node' + get "repositories/:rid/resources/:id/tree/node_from_root" => 'resources#tree_node_from_root' + + get "repositories/:rid/digital_objects/:id/tree/root" => 'digital_objects#tree_root' + get "repositories/:rid/digital_objects/:id/tree/waypoint" => 'digital_objects#tree_waypoint' + get "repositories/:rid/digital_objects/:id/tree/node" => 'digital_objects#tree_node' + get "repositories/:rid/digital_objects/:id/tree/node_from_root" => 'digital_objects#tree_node_from_root' + + get "repositories/:rid/classifications/:id/tree/root" => 'classifications#tree_root' + get "repositories/:rid/classifications/:id/tree/waypoint" => 'classifications#tree_waypoint' + get "repositories/:rid/classifications/:id/tree/node" => 'classifications#tree_node' + get "repositories/:rid/classifications/:id/tree/node_from_root" => 'classifications#tree_node_from_root' + + get '/repositories', to: 'repositories#index' + get '/search', to: 'search#search' end diff --git a/public-new/config/secrets.yml b/public-new/config/secrets.yml deleted file mode 100644 index e66dc4b439..0000000000 --- a/public-new/config/secrets.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Your secret key is used for verifying the integrity of signed cookies. -# If you change this key, all old signed cookies will become invalid! - -# Make sure the secret is at least 30 characters and all random, -# no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. - -# Make sure the secrets in this file are kept private -# if you're sharing your code publicly. - -development: - secret_key_base: 459f2198d65b6f7ea3261ba3a4141258e86d96ab34d2e32abd165f594f9fb03057324e2a9827c903cebb12fcf710f2a11ff30eb38ee02eff127dfbaeee952594 - -test: - secret_key_base: 71901406f677b0c980864a7f8146adc642267a2efe8a78a244337e0658cbea2a96aee46400c7eb8ed92e85d21dd795237d106a90734d19d12c381d711e3e94d1 - -# Do not keep production secrets in the repository, -# instead read values from the environment. -# production: -# secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> -# TODO = change this -production: - secret_key_base: <%= Digest::SHA1.hexdigest(AppConfig[:public_cookie_secret]) %> - diff --git a/public-new/config/warble.rb b/public-new/config/warble.rb index d025d46357..eb10be4c53 100644 --- a/public-new/config/warble.rb +++ b/public-new/config/warble.rb @@ -126,7 +126,7 @@ config.webxml.booter = :rails # Set JRuby to run in 1.9 mode. - config.webxml.jruby.compat.version = "1.9" + # config.webxml.jruby.compat.version = "1.9" # When using the :rack booter, "Rackup" script to use. # - For 'rackup.path', the value points to the location of the rackup diff --git a/public-new/db/seeds.rb b/public-new/db/seeds.rb index 4edb1e857e..1beea2accd 100644 --- a/public-new/db/seeds.rb +++ b/public-new/db/seeds.rb @@ -1,7 +1,7 @@ # This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # # Examples: # -# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) -# Mayor.create(name: 'Emanuel', city: cities.first) +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/public-new/design_assets/ArchivesSpace-Landsc#CBEFD5.png b/public-new/design_assets/ArchivesSpace-Landsc#CBEFD5.png deleted file mode 100644 index f327b54f49..0000000000 Binary files a/public-new/design_assets/ArchivesSpace-Landsc#CBEFD5.png and /dev/null differ diff --git a/public-new/design_assets/ArchivesSpace-Landscape.png b/public-new/design_assets/ArchivesSpace-Landscape.png deleted file mode 100644 index 239caab87b..0000000000 Binary files a/public-new/design_assets/ArchivesSpace-Landscape.png and /dev/null differ diff --git a/public-new/design_assets/ArchivesSpaceStacked.png b/public-new/design_assets/ArchivesSpaceStacked.png deleted file mode 100644 index fdee6cd83e..0000000000 Binary files a/public-new/design_assets/ArchivesSpaceStacked.png and /dev/null differ diff --git a/public-new/jasmine/fixtures/layout.html b/public-new/jasmine/fixtures/layout.html deleted file mode 100644 index ee9dcdd47e..0000000000 --- a/public-new/jasmine/fixtures/layout.html +++ /dev/null @@ -1,42 +0,0 @@ - - - -
      - -
      - -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -
      - - - - - - - - - - diff --git a/public-new/jasmine/my.conf.js b/public-new/jasmine/my.conf.js deleted file mode 100644 index dba653a040..0000000000 --- a/public-new/jasmine/my.conf.js +++ /dev/null @@ -1,110 +0,0 @@ -// Karma configuration - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: '../', - - - // plugins: [ require('karma-quixote') ], - - // frameworks to use - // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine', 'quixote'], - - - // list of files / patterns to load in the browser - files: [ - 'vendor/assets/javascripts/lodash/lodash.js', - 'vendor/assets/javascripts/lodash-inflection/lodash-inflection.js', - 'app/assets/javascripts/lodash.aspace.js', - 'vendor/assets/javascripts/jquery/jquery.js', - 'vendor/assets/javascripts/exoskeleton/exoskeleton.js', - 'vendor/assets/javascripts/backbone.paginator/backbone.paginator.js', - - 'vendor/assets/javascripts/foundation-sites/foundation.core.js', - 'vendor/assets/javascripts/foundation-sites/foundation.util.keyboard.js', - 'vendor/assets/javascripts/foundation-sites/foundation.util.box.js', - 'vendor/assets/javascripts/foundation-sites/foundation.util.triggers.js', - 'vendor/assets/javascripts/foundation-sites/foundation.util.mediaQuery.js', - 'vendor/assets/javascripts/foundation-sites/foundation.util.motion.js', - - 'vendor/assets/javascripts/foundation-sites/foundation.reveal.js', - 'vendor/assets/javascripts/foundation-sites/foundation.dropdown.js', - 'vendor/assets/javascripts/foundation-sites/foundation.accordion.js', - 'app/assets/javascripts/record-presenter.js', - 'app/assets/javascripts/*.js', - 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', - 'node_modules/jasmine-ajax/lib/mock-ajax.js', - 'node_modules/jasmine-fixture/dist/jasmine-fixture.js', - 'jasmine/spec_helper.js', - 'jasmine/*.js', - { - pattern: 'jasmine/fixtures/*.html', - watched: false, - included: false, - served: true - }, - { - pattern: 'app/assets/stylesheets/application.scss', - watched: false, - included: false, - served: true - } - ], - - - // list of files to exclude - exclude: [ - ], - - - // preprocess matching files before serving them to the browser - // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - preprocessors: { - 'app/assets/stylesheets/application.scss': ['scss'] - }, - - scssPreprocessor: { - options: { - sourceMap: true, - includePaths: ['app/assets/stylesheets', 'vendor/assets/stylesheets'] - } - }, - - - // test results reporter to use - // possible values: 'dots', 'progress' - // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress'], - - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - // level of logging - // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: true, - - - // start these browsers - // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Firefox'], - // browsers: ['Chrome'], - // browsers: ['PhantomJS2'], - - // Continuous Integration mode - // if true, Karma captures browsers, runs the tests and exits - singleRun: false - }) -} diff --git a/public-new/jasmine/readmore_spec.js b/public-new/jasmine/readmore_spec.js deleted file mode 100644 index d03f072943..0000000000 --- a/public-new/jasmine/readmore_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -describe('$.fn.readmore', function() { - - beforeEach(function() { - var $emma = affix('#emma'); - - $emma.affix("p").html("Emma Woodhouse, handsome, clever, and rich, with a comfortable home and happy disposition, seemed to unite some of the best blessings of existence; and had lived nearly twenty-one years in the world with very little to distress or vex her."); - $emma.affix("p").html("She was the youngest of the two daughters of a most affectionate, indulgent father, and had, in consequence of her sister's marriage, been mistress of his house from a very early period. Her mother had died too long ago for her to have more than an indistinct remembrance of her caresses, and her place had been supplied by an excellent woman as governess, who had fallen little short of a mother in affection."); - }); - - it("breaks text into less and more", function() { - $('#emma').readmore(20); - expect($('#emma p:nth-child(1) span.less').html()).toEqual('Emma Woodhouse, handsome,...'); - expect($('#emma p:nth-child(1) span.more').html()).toMatch(/^\sclever.*her\.$/); - expect($('#emma p:nth-child(2)')).toHaveClass('more'); - }); - - - it("doesn't insert the break point inside inline markup or count characters of which tags consist", function() { - $('#emma').readmore(535); - expect($('#emma p:nth-child(2) span.more').html()).toMatch(/^\splace\shad/); - }); - - - it("inserts a 'see more' link", function() { - $('#emma').readmore(20); - expect($('#emma p:nth-child(2)').next()[0]).toHaveClass("expander"); - }); - - - it("toggles 'expanded' class on the container when the link is clicked", function() { - $('#emma').readmore(20); - $('#emma p:nth-child(2)').next('a').trigger("click"); - expect($('#emma')).toHaveClass('expanded'); - }); - - - it("is smart enough to work with unparagraphed content too", function() { - var text = $("p", $('#emma')).html(); - - $('#emma').empty().html(text); - $('#emma').readmore(20); - console.log($('#emma').html()); - expect($('#emma span.less').html()).toEqual('Emma Woodhouse, handsome,...'); - }); - -}); diff --git a/public-new/jasmine/record_model_spec.js b/public-new/jasmine/record_model_spec.js deleted file mode 100644 index ae387b8163..0000000000 --- a/public-new/jasmine/record_model_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -describe('Record Model(s)', function() { - - beforeEach(function() { - jasmine.Ajax.install(); - }); - - beforeEach(function() { - this.resourceRecord = new app.RecordModel({ - recordTypePath: 'resource', - id: 1, - repoId: 2 - }); - - this.resourceRecord.fetch({ - error: function(model, response, options) { - model.barf = true; - } - }); - - this.request = jasmine.Ajax.requests.mostRecent(); - }); - - - afterEach(function() { - jasmine.Ajax.uninstall(); - }); - - - describe('fetching: happy path', function() { - - beforeEach(function() { - this.request.respondWith(TestResponses.resource.success); - }); - - it('fetches data from the server', function() { - expect(this.request.url).toEqual('/api/repositories/2/resources/1'); - expect(this.resourceRecord.attributes.title).toEqual("Dick Cavett Papers"); - }); - }); - - describe('fetching: tragic path', function() { - - beforeEach(function() { - this.request.respondWith(TestResponses.resource.failure); - }); - - it('accepts an error handling callback', function() { - expect(this.request.url).toEqual('/api/repositories/2/resources/1'); - expect(this.resourceRecord.barf).toEqual(true); - }); - }); - -}); diff --git a/public-new/jasmine/record_view_spec.js b/public-new/jasmine/record_view_spec.js deleted file mode 100644 index 7fe1d3ab0e..0000000000 --- a/public-new/jasmine/record_view_spec.js +++ /dev/null @@ -1,78 +0,0 @@ -describe('RecordContainerView', function() { - - function FonyRecordModel(opts) {} - - FonyRecordModel.prototype.fetch = function() { - this.attributes = { - repository: { - ref: "/what/the/foo", - _resolved: { - name: "What the FOO Repository", - agent_representation: { - _resolved: { - agent_contacts: [{ - address_1: "1 yale lane", - address_2: "apt 2", - city: "yale city", - create_time: "2016-01-27T04:23:11Z", - created_by: "admin", - email: "foo@bar.com", - jsonmodel_type: "agent_contact", - last_modified_by: "admin", - lock_version: 0, - name: "Yale University Special Collections", - region: "CT", - system_mtime: "2016-01-27T04:23:11Z", - telephones: [{ - create_time: "2016-01-27T04:23:12Z", - created_by: "admin", - jsonmodel_type: "telephone", - last_modified_by: "admin", - number: "555-1234", - number_type: "business", - system_mtime: "2016-01-27T04:23:12Z", - uri: "/telephone/1", - user_mtime: "2016-01-27T04:23:12Z", - user_mtime: "2016-01-27T04:23:11Z", - }] - }] - } - } - } - } - }; - var d = $.Deferred(); - d.resolve(true); - return d; - }; - - - beforeEach(function(done) { - app.RecordModel = FonyRecordModel; - - affix("#container"); - affix("#wait-modal[class='reveal'][data-reveal]") - var $tmpl = affix("#record-tmpl"); - - $(function() { - $(document).foundation(); - done(); - }); - }); - - it("passes a presenter object to the container", function() { - var tmplSpy = spyOn(app.utils, 'tmpl').and.returnValue(); - - var recordContainerView = new app.RecordContainerView({what: 'ever'}); - - expect(app.utils.tmpl.calls.argsFor(0)[0]).toEqual('record') - var presenter = app.utils.tmpl.calls.argsFor(0)[1]; - - expect(presenter.repository.name).toContain('What the FOO Repository'); - expect(presenter.repository.phone).toEqual("555-1234"); - expect(presenter.repository.address).toEqual("1 yale lane
      apt 2
      yale city"); - expect(presenter.repository.email).toEqual("foo@bar.com"); - - }); - -}); diff --git a/public-new/jasmine/router_spec.js b/public-new/jasmine/router_spec.js deleted file mode 100644 index 520e0bff65..0000000000 --- a/public-new/jasmine/router_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -describe('Router', function() { - - var RecordModel404 = function(opts) { - this.fetch = function() { - var d = $.Deferred(); - d.reject({barf: true}); - return d; - } - } - - var RecordModel200 = function(opts) { - this.fetch = function() { - this.title = "Foo Record"; - var d = $.Deferred(); - d.resolve(true); - return d; - } - } - - beforeEach(function() { - jasmine.getFixtures().fixturesPath = 'base/jasmine/fixtures'; - loadFixtures("layout.html"); - - jasmine.Ajax.install(); - }); - - - afterEach(function() { - jasmine.Ajax.uninstall(); - }); - - - describe("showRecord", function() { - - beforeEach(function(done) { - this.mockView = function(text) { - return { - $el: { - html: function() { - return "

      "+text+"

      "; - } - } - }; - }; - - $(function() { - $(document).foundation(); - done(); - }); - - }); - - // TODO - move to record model spec - - xit("instantiates a SeverErrorView when model fails", function(done) { - var spy = spyOn(app, 'ServerErrorView').and.returnValue( - this.mockView("BUMMER")); - - app.RecordModel = RecordModel404; - app.router.showRecord({}); - expect(spy).toHaveBeenCalled(); - expect($('#main-content')).toContainHtml("

      BUMMER

      "); - done(); - }); - - xit("instantiates a RecordView when the model loads", function(done) { - var spy = spyOn(app, 'RecordView').and.returnValue( - this.mockView("FAR OUT")); - - app.RecordModel = RecordModel200; - app.router.showRecord({}); - expect(spy).toHaveBeenCalled(); - expect($('#main-content')).toContainHtml("

      FAR OUT

      "); - done(); - }); - - }); - -}); diff --git a/public-new/jasmine/search_facets_spec.js b/public-new/jasmine/search_facets_spec.js deleted file mode 100644 index ee46129ef5..0000000000 --- a/public-new/jasmine/search_facets_spec.js +++ /dev/null @@ -1,89 +0,0 @@ -describe('SearchFacetsView', function() { - - function splitURLParams(url) { - var x= decodeURI(url).replace(/.*\?/, '').split('&'); - return(x); - } - - beforeEach(function(done) { - affix("#sidebar"); - var $tmpl = affix("#facets-tmpl"); - - $(function() { - done(); - }); - - }); - - - describe('FacetHelper', function() { - - beforeEach(function() { - this.facetData = { - "Repository":{"/repositories/3":{"label":"Ohio State University","count":28,"display_string":"Repository: /repositories/3","filter_term":"{\"repository\":\"/repositories/3\"}"}}, - "Type":{"archival_object":{"label":"Archival Object","count":27,"display_string":"Type: archival_object","filter_term":"{\"primary_type\":\"archival_object\"}"},"resource":{"label":"Collection","count":1,"display_string":"Type: resource","filter_term":"{\"primary_type\":\"resource\"}"}}, - "Subject":{"Costume designers -- United States -- 20th century":{"label":"Costume designers -- United States -- 20th century","count":1,"display_string":"Subject: Costume designers -- United States -- 20th century","filter_term":"{\"subjects\":\"Costume designers -- United States -- 20th century\"}"},"Costume designers -- United States -- 21st century":{"label":"Costume designers -- United States -- 21st century","count":1,"display_string":"Subject: Costume designers -- United States -- 21st century","filter_term":"{\"subjects\":\"Costume designers -- United States -- 21st century\"}"}}, - "Source":{}, - "Role":{}}; - }); - - it('provides an iterator function for listing usable facet groups', function() { - - var helper = new app.SearchFacetsView.prototype.FacetHelper({ - facetData: this.facetData, - totalRecords: 28 - }); - - var groups = [] - - helper.eachUsableFacetGroup(function(members, group) { - groups.push(group); - }); - - expect(groups).toEqual(["Type", "Subject"]); - - helper = new app.SearchFacetsView.prototype.FacetHelper({ - facetData: this.facetData, - totalRecords: 29 - }); - - groups = [] - - helper.eachUsableFacetGroup(function(members, group) { - groups.push(group); - }); - - expect(groups).toEqual(["Repository", "Type", "Subject"]); - }); - - }); - - - it('can create filtering links with current criteria', function() { - - var state = { - facetData: this.facetData, - totalRecords: 28, - pageSize: 40, - facetData: { - "Subject":{"Costume designers -- United States -- 20th century":{"label":"Costume designers -- United States -- 20th century","count":1,"display_string":"Subject: Costume designers -- United States -- 20th century","filter_term":"{\"subjects\":\"Costume designers -- United States -- 20th century\"}"},"Costume designers -- United States -- 21st century":{"label":"Costume designers -- United States -- 21st century","count":1,"display_string":"Subject: Costume designers -- United States -- 21st century","filter_term":"{\"subjects\":\"Costume designers -- United States -- 21st century\"}"}}, - }, - filters: [{"subjects": "all tomorrow's parties"}] - } - - var helper = new app.SearchFacetsView.prototype.FacetHelper(state); - - var addSubjectFilterURL = splitURLParams(helper.getAddFilterURL("{\"subjects\":\"Costume designers -- United States -- 21st century\"}")); - - - expect(addSubjectFilterURL).toContain("subjects=all tomorrow's parties"); - expect(addSubjectFilterURL).toContain('subjects=Costume designers -- United States -- 21st century'); - - var addRepositoryFilterURL = splitURLParams(helper.getAddFilterURL('{"repository": "/repositories/69"}')); - - expect(addRepositoryFilterURL).toContain("repository=/repositories/69"); - - }); - - -}); diff --git a/public-new/jasmine/search_form_spec.js b/public-new/jasmine/search_form_spec.js deleted file mode 100644 index 0557c5479e..0000000000 --- a/public-new/jasmine/search_form_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -describe('SearchEditor', function() { - - beforeEach(function(done) { - - affix("#search-editor-container"); - affix("#search-query-row-tmpl").affix("div.add-query-row").affix("a"); - - $(function() { - done(); - }); - }); - - - it("can bind to a DOM container and add search query rows", function() { - var $container = $("#search-editor-container"); - var editor = new app.SearchEditor($container); - - _.times(3, function() { - editor.addRow(); - }); - - expect($(".search-query-row", $container).length).toEqual(3); - }); - - it("can remove query rows and update the index accordingly", function() { - var $container = $("#search-editor-container"); - var editor = new app.SearchEditor($container); - - _.times(3, function() { - editor.addRow(); - }); - - $(".remove-query-row a", $(".search-query-row").first()).trigger("click"); - expect($(".search-query-row", $container).length).toEqual(2); - }); -}); diff --git a/public-new/jasmine/search_pager_spec.js b/public-new/jasmine/search_pager_spec.js deleted file mode 100644 index cffd4665e9..0000000000 --- a/public-new/jasmine/search_pager_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -describe('SearchPagerView', function() { - - beforeEach(function(done) { - $(function() { - done(); - }); - - }); - - - it("calls its template with a helper object for building pagination links", function() { - - var tmplSpy = spyOn(app.utils, 'tmpl').and.returnValue(); - var query = { - page: 2, - buildQueryString: function(args) { - return "/searchme?foo=bar"; - } - }; - - var buildQueryStringSpy = spyOn(query, 'buildQueryString').and.callThrough(); - - var opts = { - query: query, - resultsState: { - currentPage: 2, - totalPages: 3 - } - }; - - var searchPagerView = new app.SearchPagerView(opts); - expect(app.utils.tmpl.calls.argsFor(0)[0]).toEqual('search-pager') - var pagerHelper = app.utils.tmpl.calls.argsFor(0)[1]; - expect(pagerHelper.hasPreviousPage).toEqual(true); - expect(pagerHelper.hasNextPage).toEqual(true); - expect(pagerHelper.getPagerEnd()).toEqual(3); - expect(pagerHelper.getPagerStart()).toEqual(1); - - pagerHelper.getPreviousPageURL(); - expect(buildQueryStringSpy).toHaveBeenCalledWith({page: 1}); - - pagerHelper.getNextPageURL(); - expect(buildQueryStringSpy).toHaveBeenCalledWith({page: 3}); - - }); - -}); diff --git a/public-new/jasmine/search_query_spec.js b/public-new/jasmine/search_query_spec.js deleted file mode 100644 index 90524e33f8..0000000000 --- a/public-new/jasmine/search_query_spec.js +++ /dev/null @@ -1,26 +0,0 @@ -describe('SearchQuery', function() { - - // it("can parse a query string into search params and translate them for the API", function() { - // var queryString = 'page=1&repository=/repositories/2&repository=/repositories/3&subject=Papers&subject=Rocks'; - - // var sq = new app.SearchQuery(queryString); - // var apiParams = sq.toApi(); - - // expect(apiParams["filter_term[]"][0]).toEqual('{"repository":"/repositories/2"}'); - // expect(apiParams["filter_term[]"][1]).toEqual('{"repository":"/repositories/3"}'); - // expect(apiParams["filter_term[]"][2]).toEqual('{"subjects":"Papers"}'); - // expect(apiParams["filter_term[]"][3]).toEqual('{"subjects":"Rocks"}'); - - // }); - - - // it("will ignore supernumerary page params", function() { - // var queryString = 'page=2&page=1&subject=rocks'; - - // var sq = new app.SearchQuery(queryString); - - // expect(sq.page).toEqual(2); - // }); - - -}); diff --git a/public-new/jasmine/search_results_spec.js b/public-new/jasmine/search_results_spec.js deleted file mode 100644 index d7214861d6..0000000000 --- a/public-new/jasmine/search_results_spec.js +++ /dev/null @@ -1,176 +0,0 @@ -describe('Search Results', function() { - - function splitURLParams(url) { - var x= decodeURI(url).replace(/.*\?/, '').split('&'); - return(x); - } - - beforeEach(function() { - jasmine.Ajax.install(); - }); - - beforeEach(function() { - - this.searchResults = new app.SearchResults([], { - state: { - pageSize: 40, - currentPage: 1, - query: [{ - field: "title", - recordtype: "any", - value: "big" - }, { - field: "keyword", - op: "OR", - value: "paper" - }] - } - }); - - this.searchResults.fetch({ - // don't really need this anymore - use applyFilter method. - data: { - filter_term: ['{"repositories":"/repositories/2"}'], - } - }); - this.request = jasmine.Ajax.requests.mostRecent(); - }); - - - beforeEach(function() { - this.request.respondWith(TestResponses.search.success); - }); - - - afterEach(function() { - jasmine.Ajax.uninstall(); - }); - - - it('translates query params for the server', function() { - expect(this.request.url).toMatch(/^\/api\/search/); - var decoded = decodeURIComponent(this.request.url).replace(/.*\?/, '').split('&'); - expect(decoded).toContain('v0=big'); - expect(decoded).toContain('f0=title'); - expect(decoded).toContain('page_size=40'); - expect(decoded).toContain('page=1'); - expect(decoded).toContain('filter_term[]={"repositories":"/repositories/2"}'); - }); - - it('can fetch a new page size', function(done) { - this.searchResults.setPageSize(60); - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - expect(request.url).toContain('page_size=60'); - done(); - }); - - - it('can change sort order and re fetch', function(done) { - this.searchResults.setSorting("title"); - this.searchResults.fetch(); - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - console.log(request.url); - expect(request.url).toContain('title_sort+asc'); - done(); - }); - - - it('can update the base query and refetch', function(done) { - var newQuery = [{ - field: "title", - recordtype: "any", - value: "small" - }, { - field: "keyword", - op: "OR", - value: "paper" - }]; - - this.searchResults.updateQuery(newQuery); - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - expect(request.url).toContain('v0=small'); - done(); - }); - - - it('can apply multiple filters', function() { - this.searchResults.applyFilter({'repositories': "/repositories/99"}); - this.searchResults.applyFilter({'repositories': "/repositories/98"}); - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - var decoded = decodeURIComponent(request.url).replace(/.*\?/, '').split('&'); - expect(decoded).toContain('filter_term[]={"repositories":"/repositories/99"}'); - expect(decoded).toContain('filter_term[]={"repositories":"/repositories/98"}'); - }); - - - it('can remove a filter', function() { - this.searchResults.applyFilter({'repositories': "/repositories/99"}); - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - var decoded = decodeURIComponent(request.url).replace(/.*\?/, '').split('&'); - expect(decoded).toContain('filter_term[]={"repositories":"/repositories/99"}'); - - this.searchResults.removeFilter({'repositories': "/repositories/99"}); - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - var decoded = decodeURIComponent(request.url).replace(/.*\?/, '').split('&'); - expect(decoded).not.toContain('filter_term[]={"repositories":"/repositories/99"}'); - }); - - - it('knows how many results it has', function() { - expect(this.searchResults.state.totalRecords).toEqual(3); - }); - - - describe('Identifier searchers', function() { - - it('wraps quotes around an identifier query if it has whitespace', function() { - var idQuery = [{ - field: "identifier", - recordtype: "any", - value: "SS Minow" - }]; - - this.searchResults.updateQuery(idQuery) - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - expect(request.url).toContain('v0=%22SS+Minow%22'); - - }); - - it('bookends an id query with wildcard if it seems like the user intended to search an identifier segment', function() { - var idQuery = [{ - field: "identifier", - recordtype: "any", - value: "SS" - }]; - - this.searchResults.updateQuery(idQuery) - var request = jasmine.Ajax.requests.mostRecent(); - request.respondWith(TestResponses.search.success); - expect(request.url).toContain('v0=*SS*'); - }); - }); - - - describe('SearchResultItem', function() { - - beforeEach(function() { - this.item = this.searchResults.models[0]; - }); - - it('stores the record title in the attributes object', function() { - expect(this.item.attributes.title).toEqual("Jimmy Page Papers"); - }); - - it('generates a public-friendly url for resource records', function() { - expect(this.item.getURL()).toEqual('/repositories/13/collections/666'); - }); - - }); -}); diff --git a/public-new/jasmine/search_toolbar_spec.js b/public-new/jasmine/search_toolbar_spec.js deleted file mode 100644 index d0718cc7ef..0000000000 --- a/public-new/jasmine/search_toolbar_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -describe('SearchToolbarView', function() { - - beforeEach(function(done) { - affix("#search-box"); - var $container = affix("#search-toolbar-tmpl"); - $container.affix("div#editor-container"); - $container.affix("a#search-button"); - var $a = $container.affix("ul#numberresults").affix('li').affix("a"); - $a.text('1000'); - - $container.affix("ul#sortorder").affix('li[data-value="bar"]').affix("a"); - - $(function() { - done(); - }); - - }); - - - it("emits a changepagesize.aspace event when $('#numberresults a') elements are clicked", function() { - var searchToolbarView = new app.SearchToolbarView({ - query: {} - }); - var eventTriggered = false; - var resultsPerPage = 10; - - searchToolbarView.on("changepagesize.aspace", function(newSize) { - eventTriggered = true; - resultsPerPage = newSize; - }); - - $("#numberresults a").trigger("click"); - - expect(eventTriggered).toEqual(true); - expect(resultsPerPage).toEqual(1000); - }); - - - it("emits a changesortorder.aspace event when $('#sortorder a') elements are clicked", function() { - var searchToolbarView = new app.SearchToolbarView({ - query: {} - }); - var eventTriggered = false; - var sortOrder = 'foo'; - - searchToolbarView.on("changesortorder.aspace", function(newSortOrder) { - eventTriggered = true; - sortOrder = newSortOrder; - }); - - $("#sortorder a").trigger("click"); - - expect(eventTriggered).toEqual(true); - expect(sortOrder).toEqual('bar'); - }); - - - - it("updates the query and emits a modifiedquery.aspace event when $('#search-button') is clicked", function(done) { - var mockQuery = jasmine.createSpyObj('query', ['updateCriteria', 'buildQueryString', 'foo']); - var mockSearchEditor = jasmine.createSpyObj('searchEditor', ['extract']); - - var searchToolbarView = new app.SearchToolbarView({ - query: mockQuery - }); - - searchToolbarView.searchEditor = mockSearchEditor; - - expect(mockSearchEditor.extract).not.toHaveBeenCalled(); - expect(mockQuery.updateCriteria).not.toHaveBeenCalled(); - - searchToolbarView.on("modifiedquery.aspace", function(query) { - expect(mockSearchEditor.extract).toHaveBeenCalled(); - expect(mockQuery.updateCriteria).toHaveBeenCalled(); - expect(query.foo).toBeDefined(); //ensure we got the right object - done(); - }); - - $("#search-button").trigger("click"); - }); - - -}); diff --git a/public-new/jasmine/spec_helper.js b/public-new/jasmine/spec_helper.js deleted file mode 100644 index 54ac10600e..0000000000 --- a/public-new/jasmine/spec_helper.js +++ /dev/null @@ -1,20 +0,0 @@ -beforeEach(function() { - - // It can be easy to forget, but doing something like: - // jasmine.Ajax.install(); - // loadFixtures(); - // causes tests to silently die. This ensures that - // we have things in the right order in our setups - - if(window.loadFixtures) { - - window.loadFixtures = function() { - if (window.XMLHttpRequest.name === "FakeXMLHttpRequest") { - throw new Error("Can't load fixtures after Mock Ajax has taken over"); - } else { - jasmine.getFixtures().proxyCallTo_('load', arguments) - } - } - } - -}); diff --git a/public-new/jasmine/stylesheets_spec.js b/public-new/jasmine/stylesheets_spec.js deleted file mode 100644 index 816f3e7bfc..0000000000 --- a/public-new/jasmine/stylesheets_spec.js +++ /dev/null @@ -1,79 +0,0 @@ -// require quixote - -var frame; - -var dtiOrig = jasmine.DEFAULT_TIMEOUT_INTERVAL; -jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - -describe('stylesheets', function() { - - beforeAll(function(done) { - frame = quixote.createFrame({ - stylesheet: '/base/app/assets/stylesheets/application.css' - }); - - setTimeout(function() { - done(); - }, 5000); - - }); - - afterAll(function() { - frame.remove(); - jasmine.DEFAULT_TIMEOUT_INTERVAL = dtiOrig; - }); - - beforeEach(function() { - frame.reset(); - }); - - - it("makes

      elements darkblue", function(done) { - var DARKBLUE = "rgb(5, 83, 138)"; - - frame.add("

      FOO

      "); - - header = frame.get("#foo"); - - expect(header.getRawStyle("color")).toEqual(DARKBLUE); - - done(); - }); - - - describe("Search Editor rows", function() { - - beforeEach(function() { - frame.add("
      "); - }); - - - it("has 500 weight button text", function(done) { - - button = frame.get(".button"); - - expect(button.getRawStyle("font-weight")).toEqual('500'); - - done(); - }); - - }); - - - describe("record type badges", function() { - - beforeEach(function() { - frame.add("
       Resource
      "); - }); - - it("gives a 1 px border to record badges", function(done) { - badge = frame.get(".record-type-badge"); - expect(badge.getRawStyle("border-top-width")).toEqual('1px'); - expect(badge.getRawStyle("font-family")).toEqual('"Roboto Slab",serif'); - - done(); - }); - - }); - -}) diff --git a/public-new/jasmine/test-responses.js b/public-new/jasmine/test-responses.js deleted file mode 100644 index 30219568b4..0000000000 --- a/public-new/jasmine/test-responses.js +++ /dev/null @@ -1,60 +0,0 @@ -var TestResponses = { - search: { - success: { - status: 200, - responseText: JSON.stringify({ - facet_data: {}, - search_data: { - this_page: 1, - page_size: 40, - total_hits: 3, - results: [ - { - title: "Jimmy Page Papers", - primary_type: 'resource', - uri: "/repositories/13/resources/666", - json: JSON.stringify({ - title: "Jimmy Page Papers" - }) - }, - { - title: "Robert Plant Papers", - json: JSON.stringify({ - title: "Jimmy Page Papers" - }) - }, - { - title: "Stairway to Heaven Manuscript", - json: JSON.stringify({ - title: "Jimmy Page Papers" - }) - } - ], - criteria: { - 'filter_term[]': ['{"repositories":"/repositories/2"}'], - 'q': 'foo', - 'page_size': 40 - } - } - }) - }, - failure: { - status: 500, - responseText: 'BARF' - } - }, - - resource: { - success: { - status: 200, - responseText: JSON.stringify({ - title: "Dick Cavett Papers" - }) - }, - - failure: { - status: 500, - responseText: 'PUKE' - } - } -}; diff --git a/public-new/jasmine/utils_spec.js b/public-new/jasmine/utils_spec.js deleted file mode 100644 index 71aa18c674..0000000000 --- a/public-new/jasmine/utils_spec.js +++ /dev/null @@ -1,89 +0,0 @@ -describe('Utils', function() { - - var advancedQuery = { - query: { - jsonmodel_type: "boolean_query", - op: "OR", - subqueries: [ - { - field: "title", - jsonmodel_type: "field_query", - value: "objective" - }, - { - field: "title", - jsonmodel_type: "field_query", - value: "subjective" - } - ] - } - }; - - - var anotherQuery = { - query: { - field: "title", - jsonmodel_type: "field_query", - value: "rejective" - } - }; - - - it("can turn public app terminology into proper ASpace record type", function() { - expect(app.utils.getASType('collection')).toEqual('resource'); - expect(app.utils.getASType('accession')).toEqual('accession'); - }); - - it("can rename ASpace record type for the public app", function() { - expect(app.utils.getPublicType('resource')).toEqual('collection'); - expect(app.utils.getPublicType('accession')).toEqual('accession'); - }); - - it("can convert advanced query objects into url params", function() { - - var params = app.utils.convertAdvancedQuery(advancedQuery); - - expect(params.op1).toEqual("OR"); - expect(params.q0).toEqual("objective"); - expect(params.q1).toEqual("subjective"); - expect(params.f0).toEqual("title"); - expect(params.f1).toEqual("title"); - }); - - it("can convert advanced query objects into flat arrays", function() { - var list = app.utils.flattenAdvancedQuery(advancedQuery); - - expect(list).toEqual(["title:objective", "OR", "title:subjective"]) - }); - - it("can iterate over advanced query as a set of rows", function() { - var rowCount = 0 - app.utils.eachAdvancedQueryRow(advancedQuery, function(rowObj, i) { - if(i === 0) { - expect(rowObj.field).toEqual("title"); - expect(rowObj.value).toEqual("objective"); - } else { - expect(rowObj.op).toEqual("OR"); - expect(rowObj.value).toEqual("subjective"); - } - - rowCount += 1; - }); - - expect(rowCount).toEqual(2); - - rowCount = 0; - app.utils.eachAdvancedQueryRow(anotherQuery, function(rowObj, i) { - expect(rowObj.field).toEqual("title"); - expect(rowObj.value).toEqual("rejective"); - expect(rowObj.op).toBeUndefined(); - - rowCount += 1; - }); - - expect(rowCount).toEqual(1); - - }); - - -}); diff --git a/public-new/lodash b/public-new/lodash deleted file mode 100644 index e20df38848..0000000000 --- a/public-new/lodash +++ /dev/null @@ -1,14400 +0,0 @@ -/** - * @license - * lodash 4.0.0 (Custom Build) - * Build: `lodash -d -o ./lodash.js` - * Copyright 2012-2016 The Dojo Foundation - * Based on Underscore.js 1.8.3 - * Copyright 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license - */ -;(function() { - - /** Used as a safe reference for `undefined` in pre-ES5 environments. */ - var undefined; - - /** Used as the semantic version number. */ - var VERSION = '4.0.0'; - - /** Used to compose bitmasks for wrapper metadata. */ - var BIND_FLAG = 1, - BIND_KEY_FLAG = 2, - CURRY_BOUND_FLAG = 4, - CURRY_FLAG = 8, - CURRY_RIGHT_FLAG = 16, - PARTIAL_FLAG = 32, - PARTIAL_RIGHT_FLAG = 64, - ARY_FLAG = 128, - REARG_FLAG = 256, - FLIP_FLAG = 512; - - /** Used to compose bitmasks for comparison styles. */ - var UNORDERED_COMPARE_FLAG = 1, - PARTIAL_COMPARE_FLAG = 2; - - /** Used as default options for `_.truncate`. */ - var DEFAULT_TRUNC_LENGTH = 30, - DEFAULT_TRUNC_OMISSION = '...'; - - /** Used to detect hot functions by number of calls within a span of milliseconds. */ - var HOT_COUNT = 150, - HOT_SPAN = 16; - - /** Used as the size to enable large array optimizations. */ - var LARGE_ARRAY_SIZE = 200; - - /** Used to indicate the type of lazy iteratees. */ - var LAZY_FILTER_FLAG = 1, - LAZY_MAP_FLAG = 2, - LAZY_WHILE_FLAG = 3; - - /** Used as the `TypeError` message for "Functions" methods. */ - var FUNC_ERROR_TEXT = 'Expected a function'; - - /** Used to stand-in for `undefined` hash values. */ - var HASH_UNDEFINED = '__lodash_hash_undefined__'; - - /** Used as references for various `Number` constants. */ - var INFINITY = 1 / 0, - MAX_SAFE_INTEGER = 9007199254740991, - MAX_INTEGER = 1.7976931348623157e+308, - NAN = 0 / 0; - - /** Used as references for the maximum length and index of an array. */ - var MAX_ARRAY_LENGTH = 4294967295, - MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, - HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; - - /** Used as the internal argument placeholder. */ - var PLACEHOLDER = '__lodash_placeholder__'; - - /** `Object#toString` result references. */ - var argsTag = '[object Arguments]', - arrayTag = '[object Array]', - boolTag = '[object Boolean]', - dateTag = '[object Date]', - errorTag = '[object Error]', - funcTag = '[object Function]', - genTag = '[object GeneratorFunction]', - mapTag = '[object Map]', - numberTag = '[object Number]', - objectTag = '[object Object]', - regexpTag = '[object RegExp]', - setTag = '[object Set]', - stringTag = '[object String]', - symbolTag = '[object Symbol]', - weakMapTag = '[object WeakMap]'; - - var arrayBufferTag = '[object ArrayBuffer]', - float32Tag = '[object Float32Array]', - float64Tag = '[object Float64Array]', - int8Tag = '[object Int8Array]', - int16Tag = '[object Int16Array]', - int32Tag = '[object Int32Array]', - uint8Tag = '[object Uint8Array]', - uint8ClampedTag = '[object Uint8ClampedArray]', - uint16Tag = '[object Uint16Array]', - uint32Tag = '[object Uint32Array]'; - - /** Used to match empty string literals in compiled template source. */ - var reEmptyStringLeading = /\b__p \+= '';/g, - reEmptyStringMiddle = /\b(__p \+=) '' \+/g, - reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; - - /** Used to match HTML entities and HTML characters. */ - var reEscapedHtml = /&(?:amp|lt|gt|quot|#39|#96);/g, - reUnescapedHtml = /[&<>"'`]/g, - reHasEscapedHtml = RegExp(reEscapedHtml.source), - reHasUnescapedHtml = RegExp(reUnescapedHtml.source); - - /** Used to match template delimiters. */ - var reEscape = /<%-([\s\S]+?)%>/g, - reEvaluate = /<%([\s\S]+?)%>/g, - reInterpolate = /<%=([\s\S]+?)%>/g; - - /** Used to match property names within property paths. */ - var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, - reIsPlainProp = /^\w*$/, - rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g; - - /** Used to match `RegExp` [syntax characters](http://ecma-international.org/ecma-262/6.0/#sec-patterns). */ - var reRegExpChar = /[\\^$.*+?()[\]{}|]/g, - reHasRegExpChar = RegExp(reRegExpChar.source); - - /** Used to match leading and trailing whitespace. */ - var reTrim = /^\s+|\s+$/g, - reTrimStart = /^\s+/, - reTrimEnd = /\s+$/; - - /** Used to match backslashes in property paths. */ - var reEscapeChar = /\\(\\)?/g; - - /** Used to match [ES template delimiters](http://ecma-international.org/ecma-262/6.0/#sec-template-literal-lexical-components). */ - var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; - - /** Used to match `RegExp` flags from their coerced string values. */ - var reFlags = /\w*$/; - - /** Used to detect hexadecimal string values. */ - var reHasHexPrefix = /^0x/i; - - /** Used to detect bad signed hexadecimal string values. */ - var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; - - /** Used to detect binary string values. */ - var reIsBinary = /^0b[01]+$/i; - - /** Used to detect host constructors (Safari > 5). */ - var reIsHostCtor = /^\[object .+?Constructor\]$/; - - /** Used to detect octal string values. */ - var reIsOctal = /^0o[0-7]+$/i; - - /** Used to detect unsigned integer values. */ - var reIsUint = /^(?:0|[1-9]\d*)$/; - - /** Used to match latin-1 supplementary letters (excluding mathematical operators). */ - var reLatin1 = /[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g; - - /** Used to ensure capturing order of template delimiters. */ - var reNoMatch = /($^)/; - - /** Used to match unescaped characters in compiled string literals. */ - var reUnescapedString = /['\n\r\u2028\u2029\\]/g; - - /** Used to compose unicode character classes. */ - var rsAstralRange = '\\ud800-\\udfff', - rsComboRange = '\\u0300-\\u036f\\ufe20-\\ufe23', - rsDingbatRange = '\\u2700-\\u27bf', - rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', - rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', - rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', - rsQuoteRange = '\\u2018\\u2019\\u201c\\u201d', - rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', - rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', - rsVarRange = '\\ufe0e\\ufe0f', - rsBreakRange = rsMathOpRange + rsNonCharRange + rsQuoteRange + rsSpaceRange; - - /** Used to compose unicode capture groups. */ - var rsAstral = '[' + rsAstralRange + ']', - rsBreak = '[' + rsBreakRange + ']', - rsCombo = '[' + rsComboRange + ']', - rsDigits = '\\d+', - rsDingbat = '[' + rsDingbatRange + ']', - rsLower = '[' + rsLowerRange + ']', - rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', - rsModifier = '(?:\\ud83c[\\udffb-\\udfff])', - rsNonAstral = '[^' + rsAstralRange + ']', - rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', - rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', - rsUpper = '[' + rsUpperRange + ']', - rsZWJ = '\\u200d'; - - /** Used to compose unicode regexes. */ - var rsLowerMisc = '(?:' + rsLower + '|' + rsMisc + ')', - rsUpperMisc = '(?:' + rsUpper + '|' + rsMisc + ')', - reOptMod = rsModifier + '?', - rsOptVar = '[' + rsVarRange + ']?', - rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', - rsSeq = rsOptVar + reOptMod + rsOptJoin, - rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, - rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; - - /** Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). */ - var reComboMark = RegExp(rsCombo, 'g'); - - /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ - var reComplexSymbol = RegExp(rsSymbol + rsSeq, 'g'); - - /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ - var reHasComplexSymbol = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); - - /** Used to match non-compound words composed of alphanumeric characters. */ - var reBasicWord = /[a-zA-Z0-9]+/g; - - /** Used to match complex or compound words. */ - var reComplexWord = RegExp([ - rsUpper + '?' + rsLower + '+(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', - rsUpperMisc + '+(?=' + [rsBreak, rsUpper + rsLowerMisc, '$'].join('|') + ')', - rsUpper + '?' + rsLowerMisc + '+', - rsDigits + '(?:' + rsLowerMisc + '+)?', - rsEmoji - ].join('|'), 'g'); - - /** Used to detect strings that need a more robust regexp to match words. */ - var reHasComplexWord = /[a-z][A-Z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; - - /** Used to assign default `context` object properties. */ - var contextProps = [ - 'Array', 'Date', 'Error', 'Float32Array', 'Float64Array', 'Function', - 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object', - 'Reflect', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array', - 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', '_', - 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout' - ]; - - /** Used to make template sourceURLs easier to identify. */ - var templateCounter = -1; - - /** Used to identify `toStringTag` values of typed arrays. */ - var typedArrayTags = {}; - typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = - typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = - typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = - typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = - typedArrayTags[uint32Tag] = true; - typedArrayTags[argsTag] = typedArrayTags[arrayTag] = - typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = - typedArrayTags[dateTag] = typedArrayTags[errorTag] = - typedArrayTags[funcTag] = typedArrayTags[mapTag] = - typedArrayTags[numberTag] = typedArrayTags[objectTag] = - typedArrayTags[regexpTag] = typedArrayTags[setTag] = - typedArrayTags[stringTag] = typedArrayTags[weakMapTag] = false; - - /** Used to identify `toStringTag` values supported by `_.clone`. */ - var cloneableTags = {}; - cloneableTags[argsTag] = cloneableTags[arrayTag] = - cloneableTags[arrayBufferTag] = cloneableTags[boolTag] = - cloneableTags[dateTag] = cloneableTags[float32Tag] = - cloneableTags[float64Tag] = cloneableTags[int8Tag] = - cloneableTags[int16Tag] = cloneableTags[int32Tag] = - cloneableTags[mapTag] = cloneableTags[numberTag] = - cloneableTags[objectTag] = cloneableTags[regexpTag] = - cloneableTags[setTag] = cloneableTags[stringTag] = - cloneableTags[symbolTag] = cloneableTags[uint8Tag] = - cloneableTags[uint8ClampedTag] = cloneableTags[uint16Tag] = - cloneableTags[uint32Tag] = true; - cloneableTags[errorTag] = cloneableTags[funcTag] = - cloneableTags[weakMapTag] = false; - - /** Used to map latin-1 supplementary letters to basic latin letters. */ - var deburredLetters = { - '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', - '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', - '\xc7': 'C', '\xe7': 'c', - '\xd0': 'D', '\xf0': 'd', - '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', - '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', - '\xcC': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', - '\xeC': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', - '\xd1': 'N', '\xf1': 'n', - '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', - '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', - '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', - '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', - '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', - '\xc6': 'Ae', '\xe6': 'ae', - '\xde': 'Th', '\xfe': 'th', - '\xdf': 'ss' - }; - - /** Used to map characters to HTML entities. */ - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '`': '`' - }; - - /** Used to map HTML entities to characters. */ - var htmlUnescapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'", - '`': '`' - }; - - /** Used to determine if values are of the language type `Object`. */ - var objectTypes = { - 'function': true, - 'object': true - }; - - /** Used to escape characters for inclusion in compiled string literals. */ - var stringEscapes = { - '\\': '\\', - "'": "'", - '\n': 'n', - '\r': 'r', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - /** Built-in method references without a dependency on `root`. */ - var freeParseFloat = parseFloat, - freeParseInt = parseInt; - - /** Detect free variable `exports`. */ - var freeExports = (objectTypes[typeof exports] && exports && !exports.nodeType) ? exports : null; - - /** Detect free variable `module`. */ - var freeModule = (objectTypes[typeof module] && module && !module.nodeType) ? module : null; - - /** Detect free variable `global` from Node.js. */ - var freeGlobal = checkGlobal(freeExports && freeModule && typeof global == 'object' && global); - - /** Detect free variable `self`. */ - var freeSelf = checkGlobal(objectTypes[typeof self] && self); - - /** Detect free variable `window`. */ - var freeWindow = checkGlobal(objectTypes[typeof window] && window); - - /** Detect the popular CommonJS extension `module.exports`. */ - var moduleExports = (freeModule && freeModule.exports === freeExports) ? freeExports : null; - - /** Detect `this` as the global object. */ - var thisGlobal = checkGlobal(objectTypes[typeof this] && this); - - /** - * Used as a reference to the global object. - * - * The `this` value is used if it's the global object to avoid Greasemonkey's - * restricted `window` object, otherwise the `window` object is used. - */ - var root = freeGlobal || ((freeWindow !== (thisGlobal && thisGlobal.window)) && freeWindow) || freeSelf || thisGlobal || Function('return this')(); - - /*--------------------------------------------------------------------------*/ - - /** - * Adds the key-value `pair` to `map`. - * - * @private - * @param {Object} map The map to modify. - * @param {Array} pair The key-value pair to add. - * @returns {Object} Returns `map`. - */ - function addMapEntry(map, pair) { - map.set(pair[0], pair[1]); - return map; - } - - /** - * Adds `value` to `set`. - * - * @private - * @param {Object} set The set to modify. - * @param {*} value The value to add. - * @returns {Object} Returns `set`. - */ - function addSetEntry(set, value) { - set.add(value); - return set; - } - - /** - * A faster alternative to `Function#apply`, this function invokes `func` - * with the `this` binding of `thisArg` and the arguments of `args`. - * - * @private - * @param {Function} func The function to invoke. - * @param {*} thisArg The `this` binding of `func`. - * @param {...*} [args] The arguments to invoke `func` with. - * @returns {*} Returns the result of `func`. - */ - function apply(func, thisArg, args) { - var length = args ? args.length : 0; - switch (length) { - case 0: return func.call(thisArg); - case 1: return func.call(thisArg, args[0]); - case 2: return func.call(thisArg, args[0], args[1]); - case 3: return func.call(thisArg, args[0], args[1], args[2]); - } - return func.apply(thisArg, args); - } - - /** - * Creates a new array concatenating `array` with `other`. - * - * @private - * @param {Array} array The first array to concatenate. - * @param {Array} other The second array to concatenate. - * @returns {Array} Returns the new concatenated array. - */ - function arrayConcat(array, other) { - var index = -1, - length = array.length, - othIndex = -1, - othLength = other.length, - result = Array(length + othLength); - - while (++index < length) { - result[index] = array[index]; - } - while (++othIndex < othLength) { - result[index++] = other[othIndex]; - } - return result; - } - - /** - * A specialized version of `_.forEach` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns `array`. - */ - function arrayEach(array, iteratee) { - var index = -1, - length = array.length; - - while (++index < length) { - if (iteratee(array[index], index, array) === false) { - break; - } - } - return array; - } - - /** - * A specialized version of `_.forEachRight` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns `array`. - */ - function arrayEachRight(array, iteratee) { - var length = array.length; - - while (length--) { - if (iteratee(array[length], length, array) === false) { - break; - } - } - return array; - } - - /** - * A specialized version of `_.every` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if all elements pass the predicate check, else `false`. - */ - function arrayEvery(array, predicate) { - var index = -1, - length = array.length; - - while (++index < length) { - if (!predicate(array[index], index, array)) { - return false; - } - } - return true; - } - - /** - * A specialized version of `_.filter` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - */ - function arrayFilter(array, predicate) { - var index = -1, - length = array.length, - resIndex = -1, - result = []; - - while (++index < length) { - var value = array[index]; - if (predicate(value, index, array)) { - result[++resIndex] = value; - } - } - return result; - } - - /** - * A specialized version of `_.includes` for arrays without support for - * specifying an index to search from. - * - * @private - * @param {Array} array The array to search. - * @param {*} target The value to search for. - * @returns {boolean} Returns `true` if `target` is found, else `false`. - */ - function arrayIncludes(array, value) { - return !!array.length && baseIndexOf(array, value, 0) > -1; - } - - /** - * A specialized version of `_.includesWith` for arrays without support for - * specifying an index to search from. - * - * @private - * @param {Array} array The array to search. - * @param {*} target The value to search for. - * @param {Function} comparator The comparator invoked per element. - * @returns {boolean} Returns `true` if `target` is found, else `false`. - */ - function arrayIncludesWith(array, value, comparator) { - var index = -1, - length = array.length; - - while (++index < length) { - if (comparator(value, array[index])) { - return true; - } - } - return false; - } - - /** - * A specialized version of `_.map` for arrays without support for iteratee - * shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - */ - function arrayMap(array, iteratee) { - var index = -1, - length = array.length, - result = Array(length); - - while (++index < length) { - result[index] = iteratee(array[index], index, array); - } - return result; - } - - /** - * Appends the elements of `values` to `array`. - * - * @private - * @param {Array} array The array to modify. - * @param {Array} values The values to append. - * @returns {Array} Returns `array`. - */ - function arrayPush(array, values) { - var index = -1, - length = values.length, - offset = array.length; - - while (++index < length) { - array[offset + index] = values[index]; - } - return array; - } - - /** - * A specialized version of `_.reduce` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @param {boolean} [initFromArray] Specify using the first element of `array` as the initial value. - * @returns {*} Returns the accumulated value. - */ - function arrayReduce(array, iteratee, accumulator, initFromArray) { - var index = -1, - length = array.length; - - if (initFromArray && length) { - accumulator = array[++index]; - } - while (++index < length) { - accumulator = iteratee(accumulator, array[index], index, array); - } - return accumulator; - } - - /** - * A specialized version of `_.reduceRight` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @param {boolean} [initFromArray] Specify using the last element of `array` as the initial value. - * @returns {*} Returns the accumulated value. - */ - function arrayReduceRight(array, iteratee, accumulator, initFromArray) { - var length = array.length; - if (initFromArray && length) { - accumulator = array[--length]; - } - while (length--) { - accumulator = iteratee(accumulator, array[length], length, array); - } - return accumulator; - } - - /** - * A specialized version of `_.some` for arrays without support for iteratee - * shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if any element passes the predicate check, else `false`. - */ - function arraySome(array, predicate) { - var index = -1, - length = array.length; - - while (++index < length) { - if (predicate(array[index], index, array)) { - return true; - } - } - return false; - } - - /** - * The base implementation of methods like `_.max` and `_.min` which accepts a - * `comparator` to determine the extremum value. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The iteratee invoked per iteration. - * @param {Function} comparator The comparator used to compare values. - * @returns {*} Returns the extremum value. - */ - function baseExtremum(array, iteratee, comparator) { - var index = -1, - length = array.length; - - while (++index < length) { - var value = array[index], - current = iteratee(value); - - if (current != null && (computed === undefined - ? current === current - : comparator(current, computed) - )) { - var computed = current, - result = value; - } - } - return result; - } - - /** - * The base implementation of methods like `_.find` and `_.findKey`, without - * support for iteratee shorthands, which iterates over `collection` using - * the provided `eachFunc`. - * - * @private - * @param {Array|Object} collection The collection to search. - * @param {Function} predicate The function invoked per iteration. - * @param {Function} eachFunc The function to iterate over `collection`. - * @param {boolean} [retKey] Specify returning the key of the found element instead of the element itself. - * @returns {*} Returns the found element or its key, else `undefined`. - */ - function baseFind(collection, predicate, eachFunc, retKey) { - var result; - eachFunc(collection, function(value, key, collection) { - if (predicate(value, key, collection)) { - result = retKey ? key : value; - return false; - } - }); - return result; - } - - /** - * The base implementation of `_.findIndex` and `_.findLastIndex` without - * support for iteratee shorthands. - * - * @private - * @param {Array} array The array to search. - * @param {Function} predicate The function invoked per iteration. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function baseFindIndex(array, predicate, fromRight) { - var length = array.length, - index = fromRight ? length : -1; - - while ((fromRight ? index-- : ++index < length)) { - if (predicate(array[index], index, array)) { - return index; - } - } - return -1; - } - - /** - * The base implementation of `_.indexOf` without `fromIndex` bounds checks. - * - * @private - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @param {number} fromIndex The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function baseIndexOf(array, value, fromIndex) { - if (value !== value) { - return indexOfNaN(array, fromIndex); - } - var index = fromIndex - 1, - length = array.length; - - while (++index < length) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * The base implementation of `_.reduce` and `_.reduceRight`, without support - * for iteratee shorthands, which iterates over `collection` using the provided - * `eachFunc`. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} accumulator The initial value. - * @param {boolean} initFromCollection Specify using the first or last element of `collection` as the initial value. - * @param {Function} eachFunc The function to iterate over `collection`. - * @returns {*} Returns the accumulated value. - */ - function baseReduce(collection, iteratee, accumulator, initFromCollection, eachFunc) { - eachFunc(collection, function(value, index, collection) { - accumulator = initFromCollection - ? (initFromCollection = false, value) - : iteratee(accumulator, value, index, collection); - }); - return accumulator; - } - - /** - * The base implementation of `_.sortBy` which uses `comparer` to define - * the sort order of `array` and replaces criteria objects with their - * corresponding values. - * - * @private - * @param {Array} array The array to sort. - * @param {Function} comparer The function to define sort order. - * @returns {Array} Returns `array`. - */ - function baseSortBy(array, comparer) { - var length = array.length; - - array.sort(comparer); - while (length--) { - array[length] = array[length].value; - } - return array; - } - - /** - * The base implementation of `_.sum` without support for iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {number} Returns the sum. - */ - function baseSum(array, iteratee) { - var result, - index = -1, - length = array.length; - - while (++index < length) { - var current = iteratee(array[index]); - if (current !== undefined) { - result = result === undefined ? current : (result + current); - } - } - return result; - } - - /** - * The base implementation of `_.times` without support for iteratee shorthands - * or max array length checks. - * - * @private - * @param {number} n The number of times to invoke `iteratee`. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the array of results. - */ - function baseTimes(n, iteratee) { - var index = -1, - result = Array(n); - - while (++index < n) { - result[index] = iteratee(index); - } - return result; - } - - /** - * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array - * of key-value pairs for `object` corresponding to the property names of `props`. - * - * @private - * @param {Object} object The object to query. - * @param {Array} props The property names to get values for. - * @returns {Object} Returns the new array of key-value pairs. - */ - function baseToPairs(object, props) { - return arrayMap(props, function(key) { - return [key, object[key]]; - }); - } - - /** - * The base implementation of `_.unary` without support for storing wrapper metadata. - * - * @private - * @param {Function} func The function to cap arguments for. - * @returns {Function} Returns the new function. - */ - function baseUnary(func) { - return function(value) { - return func(value); - }; - } - - /** - * The base implementation of `_.values` and `_.valuesIn` which creates an - * array of `object` property values corresponding to the property names - * of `props`. - * - * @private - * @param {Object} object The object to query. - * @param {Array} props The property names to get values for. - * @returns {Object} Returns the array of property values. - */ - function baseValues(object, props) { - return arrayMap(props, function(key) { - return object[key]; - }); - } - - /** - * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol - * that is not found in the character symbols. - * - * @private - * @param {Array} strSymbols The string symbols to inspect. - * @param {Array} chrSymbols The character symbols to find. - * @returns {number} Returns the index of the first unmatched string symbol. - */ - function charsStartIndex(strSymbols, chrSymbols) { - var index = -1, - length = strSymbols.length; - - while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} - return index; - } - - /** - * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol - * that is not found in the character symbols. - * - * @private - * @param {Array} strSymbols The string symbols to inspect. - * @param {Array} chrSymbols The character symbols to find. - * @returns {number} Returns the index of the last unmatched string symbol. - */ - function charsEndIndex(strSymbols, chrSymbols) { - var index = strSymbols.length; - - while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} - return index; - } - - /** - * Checks if `value` is a global object. - * - * @private - * @param {*} value The value to check. - * @returns {null|Object} Returns `value` if it's a global object, else `null`. - */ - function checkGlobal(value) { - return (value && value.Object === Object) ? value : null; - } - - /** - * Compares values to sort them in ascending order. - * - * @private - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {number} Returns the sort order indicator for `value`. - */ - function compareAscending(value, other) { - if (value !== other) { - var valIsNull = value === null, - valIsUndef = value === undefined, - valIsReflexive = value === value; - - var othIsNull = other === null, - othIsUndef = other === undefined, - othIsReflexive = other === other; - - if ((value > other && !othIsNull) || !valIsReflexive || - (valIsNull && !othIsUndef && othIsReflexive) || - (valIsUndef && othIsReflexive)) { - return 1; - } - if ((value < other && !valIsNull) || !othIsReflexive || - (othIsNull && !valIsUndef && valIsReflexive) || - (othIsUndef && valIsReflexive)) { - return -1; - } - } - return 0; - } - - /** - * Used by `_.orderBy` to compare multiple properties of a value to another - * and stable sort them. - * - * If `orders` is unspecified, all values are sorted in ascending order. Otherwise, - * specify an order of "desc" for descending or "asc" for ascending sort order - * of corresponding values. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {boolean[]|string[]} orders The order to sort by for each property. - * @returns {number} Returns the sort order indicator for `object`. - */ - function compareMultiple(object, other, orders) { - var index = -1, - objCriteria = object.criteria, - othCriteria = other.criteria, - length = objCriteria.length, - ordersLength = orders.length; - - while (++index < length) { - var result = compareAscending(objCriteria[index], othCriteria[index]); - if (result) { - if (index >= ordersLength) { - return result; - } - var order = orders[index]; - return result * (order == 'desc' ? -1 : 1); - } - } - // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications - // that causes it, under certain circumstances, to provide the same value for - // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247 - // for more details. - // - // This also ensures a stable sort in V8 and other engines. - // See https://code.google.com/p/v8/issues/detail?id=90 for more details. - return object.index - other.index; - } - - /** - * Used by `_.deburr` to convert latin-1 supplementary letters to basic latin letters. - * - * @private - * @param {string} letter The matched letter to deburr. - * @returns {string} Returns the deburred letter. - */ - function deburrLetter(letter) { - return deburredLetters[letter]; - } - - /** - * Used by `_.escape` to convert characters to HTML entities. - * - * @private - * @param {string} chr The matched character to escape. - * @returns {string} Returns the escaped character. - */ - function escapeHtmlChar(chr) { - return htmlEscapes[chr]; - } - - /** - * Used by `_.template` to escape characters for inclusion in compiled string literals. - * - * @private - * @param {string} chr The matched character to escape. - * @returns {string} Returns the escaped character. - */ - function escapeStringChar(chr) { - return '\\' + stringEscapes[chr]; - } - - /** - * Gets the index at which the first occurrence of `NaN` is found in `array`. - * - * @private - * @param {Array} array The array to search. - * @param {number} fromIndex The index to search from. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {number} Returns the index of the matched `NaN`, else `-1`. - */ - function indexOfNaN(array, fromIndex, fromRight) { - var length = array.length, - index = fromIndex + (fromRight ? 0 : -1); - - while ((fromRight ? index-- : ++index < length)) { - var other = array[index]; - if (other !== other) { - return index; - } - } - return -1; - } - - /** - * Checks if `value` is a host object in IE < 9. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a host object, else `false`. - */ - function isHostObject(value) { - // Many host objects are `Object` objects that can coerce to strings - // despite having improperly defined `toString` methods. - var result = false; - if (value != null && typeof value.toString != 'function') { - try { - result = !!(value + ''); - } catch (e) {} - } - return result; - } - - /** - * Checks if `value` is a valid array-like index. - * - * @private - * @param {*} value The value to check. - * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. - * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. - */ - function isIndex(value, length) { - value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1; - length = length == null ? MAX_SAFE_INTEGER : length; - return value > -1 && value % 1 == 0 && value < length; - } - - /** - * Converts `iterator` to an array. - * - * @private - * @param {Object} iterator The iterator to convert. - * @returns {Array} Returns the converted array. - */ - function iteratorToArray(iterator) { - var data, - result = []; - - while (!(data = iterator.next()).done) { - result.push(data.value); - } - return result; - } - - /** - * Converts `map` to an array. - * - * @private - * @param {Object} map The map to convert. - * @returns {Array} Returns the converted array. - */ - function mapToArray(map) { - var index = -1, - result = Array(map.size); - - map.forEach(function(value, key) { - result[++index] = [key, value]; - }); - return result; - } - - /** - * Replaces all `placeholder` elements in `array` with an internal placeholder - * and returns an array of their indexes. - * - * @private - * @param {Array} array The array to modify. - * @param {*} placeholder The placeholder to replace. - * @returns {Array} Returns the new array of placeholder indexes. - */ - function replaceHolders(array, placeholder) { - var index = -1, - length = array.length, - resIndex = -1, - result = []; - - while (++index < length) { - if (array[index] === placeholder) { - array[index] = PLACEHOLDER; - result[++resIndex] = index; - } - } - return result; - } - - /** - * Converts `set` to an array. - * - * @private - * @param {Object} set The set to convert. - * @returns {Array} Returns the converted array. - */ - function setToArray(set) { - var index = -1, - result = Array(set.size); - - set.forEach(function(value) { - result[++index] = value; - }); - return result; - } - - /** - * Gets the number of symbols in `string`. - * - * @param {string} string The string to inspect. - * @returns {number} Returns the string size. - */ - function stringSize(string) { - if (!(string && reHasComplexSymbol.test(string))) { - return string.length; - } - var result = reComplexSymbol.lastIndex = 0; - while (reComplexSymbol.test(string)) { - result++; - } - return result; - } - - /** - * Converts `string` to an array. - * - * @private - * @param {string} string The string to convert. - * @returns {Array} Returns the converted array. - */ - function stringToArray(string) { - return string.match(reComplexSymbol); - } - - /** - * Used by `_.unescape` to convert HTML entities to characters. - * - * @private - * @param {string} chr The matched character to unescape. - * @returns {string} Returns the unescaped character. - */ - function unescapeHtmlChar(chr) { - return htmlUnescapes[chr]; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Create a new pristine `lodash` function using the `context` object. - * - * @static - * @memberOf _ - * @category Util - * @param {Object} [context=root] The context object. - * @returns {Function} Returns a new `lodash` function. - * @example - * - * _.mixin({ 'foo': _.constant('foo') }); - * - * var lodash = _.runInContext(); - * lodash.mixin({ 'bar': lodash.constant('bar') }); - * - * _.isFunction(_.foo); - * // => true - * _.isFunction(_.bar); - * // => false - * - * lodash.isFunction(lodash.foo); - * // => false - * lodash.isFunction(lodash.bar); - * // => true - * - * // using `context` to mock `Date#getTime` use in `_.now` - * var mock = _.runInContext({ - * 'Date': function() { - * return { 'getTime': getTimeMock }; - * } - * }); - * - * // or creating a suped-up `defer` in Node.js - * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer; - */ - function runInContext(context) { - context = context ? _.defaults({}, context, _.pick(root, contextProps)) : root; - - /** Built-in constructor references. */ - var Date = context.Date, - Error = context.Error, - Math = context.Math, - RegExp = context.RegExp, - TypeError = context.TypeError; - - /** Used for built-in method references. */ - var arrayProto = context.Array.prototype, - objectProto = context.Object.prototype; - - /** Used to resolve the decompiled source of functions. */ - var funcToString = context.Function.prototype.toString; - - /** Used to check objects for own properties. */ - var hasOwnProperty = objectProto.hasOwnProperty; - - /** Used to generate unique IDs. */ - var idCounter = 0; - - /** Used to infer the `Object` constructor. */ - var objectCtorString = funcToString.call(Object); - - /** - * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) - * of values. - */ - var objectToString = objectProto.toString; - - /** Used to restore the original `_` reference in `_.noConflict`. */ - var oldDash = root._; - - /** Used to detect if a method is native. */ - var reIsNative = RegExp('^' + - funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') - .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' - ); - - /** Built-in value references. */ - var _Symbol = context.Symbol, - Reflect = context.Reflect, - Uint8Array = context.Uint8Array, - clearTimeout = context.clearTimeout, - enumerate = Reflect ? Reflect.enumerate : undefined, - getPrototypeOf = Object.getPrototypeOf, - getOwnPropertySymbols = Object.getOwnPropertySymbols, - iteratorSymbol = typeof (iteratorSymbol = _Symbol && _Symbol.iterator) == 'symbol' ? iteratorSymbol : undefined, - propertyIsEnumerable = objectProto.propertyIsEnumerable, - setTimeout = context.setTimeout, - splice = arrayProto.splice; - - /* Built-in method references for those with the same name as other `lodash` methods. */ - var nativeCeil = Math.ceil, - nativeFloor = Math.floor, - nativeIsFinite = context.isFinite, - nativeJoin = arrayProto.join, - nativeKeys = Object.keys, - nativeMax = Math.max, - nativeMin = Math.min, - nativeParseInt = context.parseInt, - nativeRandom = Math.random, - nativeReverse = arrayProto.reverse; - - /* Built-in method references that are verified to be native. */ - var Map = getNative(context, 'Map'), - Set = getNative(context, 'Set'), - WeakMap = getNative(context, 'WeakMap'), - nativeCreate = getNative(Object, 'create'); - - /** Used to store function metadata. */ - var metaMap = WeakMap && new WeakMap; - - /** Used to detect maps and sets. */ - var mapCtorString = Map ? funcToString.call(Map) : '', - setCtorString = Set ? funcToString.call(Set) : ''; - - /** Used to convert symbols to primitives and strings. */ - var symbolProto = _Symbol ? _Symbol.prototype : undefined, - symbolValueOf = _Symbol ? symbolProto.valueOf : undefined, - symbolToString = _Symbol ? symbolProto.toString : undefined; - - /** Used to lookup unminified function names. */ - var realNames = {}; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object which wraps `value` to enable implicit method - * chaining. Methods that operate on and return arrays, collections, and - * functions can be chained together. Methods that retrieve a single value or - * may return a primitive value will automatically end the chain sequence and - * return the unwrapped value. Otherwise, the value must be unwrapped with - * `_#value`. - * - * Explicit chaining, which must be unwrapped with `_#value` in all cases, - * may be enabled using `_.chain`. - * - * The execution of chained methods is lazy, that is, it's deferred until - * `_#value` is implicitly or explicitly called. - * - * Lazy evaluation allows several methods to support shortcut fusion. Shortcut - * fusion is an optimization to merge iteratee calls; this avoids the creation - * of intermediate arrays and can greatly reduce the number of iteratee executions. - * Sections of a chain sequence qualify for shortcut fusion if the section is - * applied to an array of at least two hundred elements and any iteratees - * accept only one argument. The heuristic for whether a section qualifies - * for shortcut fusion is subject to change. - * - * Chaining is supported in custom builds as long as the `_#value` method is - * directly or indirectly included in the build. - * - * In addition to lodash methods, wrappers have `Array` and `String` methods. - * - * The wrapper `Array` methods are: - * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift` - * - * The wrapper `String` methods are: - * `replace` and `split` - * - * The wrapper methods that support shortcut fusion are: - * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`, - * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`, - * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray` - * - * The chainable wrapper methods are: - * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, - * `at`, `before`, `bind`, `bindAll`, `bindKey`, `chain`, `chunk`, `commit`, - * `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`, `curry`, - * `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`, `difference`, - * `differenceBy`, `differenceWith`, `drop`, `dropRight`, `dropRightWhile`, - * `dropWhile`, `fill`, `filter`, `flatten`, `flattenDeep`, `flip`, `flow`, - * `flowRight`, `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, - * `forOwnRight`, `fromPairs`, `functions`, `functionsIn`, `groupBy`, `initial`, - * `intersection`, `intersectionBy`, `intersectionWith`, invert`, `invokeMap`, - * `iteratee`, `keyBy`, `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, - * `matches`, `matchesProperty`, `memoize`, `merge`, `mergeWith`, `method`, - * `methodOf`, `mixin`, `negate`, `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, - * `over`, `overArgs`, `overEvery`, `overSome`, `partial`, `partialRight`, - * `partition`, `pick`, `pickBy`, `plant`, `property`, `propertyOf`, `pull`, - * `pullAll`, `pullAllBy`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, - * `reject`, `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, - * `shuffle`, `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, - * `takeRight`, `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, - * `toArray`, `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, - * `unary`, `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, - * `unset`, `unshift`, `unzip`, `unzipWith`, `values`, `valuesIn`, `without`, - * `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, `zipObject`, and `zipWith` - * - * The wrapper methods that are **not** chainable by default are: - * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`, - * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `deburr`, `endsWith`, `eq`, - * `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, - * `findLast`, `findLastIndex`, `findLastKey`, `floor`, `get`, `gt`, `gte`, - * `has`, `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, - * `invoke`, `isArguments`, `isArray`, `isArrayLike`, `isArrayLikeObject`, - * `isBoolean`, `isDate`, `isElement`, `isEmpty`, `isEqual`, `isEqualWith`, - * `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`, `isMatch`, - * `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`, `isNumber`, - * `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`, `isSafeInteger`, - * `isString`, `isUndefined`, `isTypedArray`, `join`, `kebabCase`, `last`, - * `lastIndexOf`, `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, - * `mean`, `min`, `minBy`, `noConflict`, `noop`, `now`, `pad`, `padEnd`, - * `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, `repeat`, - * `result`, `round`, `runInContext`, `sample`, `shift`, `size`, `snakeCase`, - * `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`, `sortedLastIndexBy`, - * `startCase`, `startsWith`, `subtract`, `sum`, sumBy`, `template`, `times`, - * `toLower`, `toInteger`, `toLength`, `toNumber`, `toSafeInteger`, toString`, - * `toUpper`, `trim`, `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, - * `upperCase`, `upperFirst`, `value`, and `words` - * - * @name _ - * @constructor - * @category Seq - * @param {*} value The value to wrap in a `lodash` instance. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * function square(n) { - * return n * n; - * } - * - * var wrapped = _([1, 2, 3]); - * - * // returns an unwrapped value - * wrapped.reduce(_.add); - * // => 6 - * - * // returns a wrapped value - * var squares = wrapped.map(square); - * - * _.isArray(squares); - * // => false - * - * _.isArray(squares.value()); - * // => true - */ - function lodash(value) { - if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { - if (value instanceof LodashWrapper) { - return value; - } - if (hasOwnProperty.call(value, '__wrapped__')) { - return wrapperClone(value); - } - } - return new LodashWrapper(value); - } - - /** - * The function whose prototype all chaining wrappers inherit from. - * - * @private - */ - function baseLodash() { - // No operation performed. - } - - /** - * The base constructor for creating `lodash` wrapper objects. - * - * @private - * @param {*} value The value to wrap. - * @param {boolean} [chainAll] Enable chaining for all wrapper methods. - */ - function LodashWrapper(value, chainAll) { - this.__wrapped__ = value; - this.__actions__ = []; - this.__chain__ = !!chainAll; - this.__index__ = 0; - this.__values__ = undefined; - } - - /** - * By default, the template delimiters used by lodash are like those in - * embedded Ruby (ERB). Change the following template settings to use - * alternative delimiters. - * - * @static - * @memberOf _ - * @type Object - */ - lodash.templateSettings = { - - /** - * Used to detect `data` property values to be HTML-escaped. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'escape': reEscape, - - /** - * Used to detect code to be evaluated. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'evaluate': reEvaluate, - - /** - * Used to detect `data` property values to inject. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'interpolate': reInterpolate, - - /** - * Used to reference the data object in the template text. - * - * @memberOf _.templateSettings - * @type string - */ - 'variable': '', - - /** - * Used to import variables into the compiled template. - * - * @memberOf _.templateSettings - * @type Object - */ - 'imports': { - - /** - * A reference to the `lodash` function. - * - * @memberOf _.templateSettings.imports - * @type Function - */ - '_': lodash - } - }; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation. - * - * @private - * @param {*} value The value to wrap. - */ - function LazyWrapper(value) { - this.__wrapped__ = value; - this.__actions__ = []; - this.__dir__ = 1; - this.__filtered__ = false; - this.__iteratees__ = []; - this.__takeCount__ = MAX_ARRAY_LENGTH; - this.__views__ = []; - } - - /** - * Creates a clone of the lazy wrapper object. - * - * @private - * @name clone - * @memberOf LazyWrapper - * @returns {Object} Returns the cloned `LazyWrapper` object. - */ - function lazyClone() { - var result = new LazyWrapper(this.__wrapped__); - result.__actions__ = copyArray(this.__actions__); - result.__dir__ = this.__dir__; - result.__filtered__ = this.__filtered__; - result.__iteratees__ = copyArray(this.__iteratees__); - result.__takeCount__ = this.__takeCount__; - result.__views__ = copyArray(this.__views__); - return result; - } - - /** - * Reverses the direction of lazy iteration. - * - * @private - * @name reverse - * @memberOf LazyWrapper - * @returns {Object} Returns the new reversed `LazyWrapper` object. - */ - function lazyReverse() { - if (this.__filtered__) { - var result = new LazyWrapper(this); - result.__dir__ = -1; - result.__filtered__ = true; - } else { - result = this.clone(); - result.__dir__ *= -1; - } - return result; - } - - /** - * Extracts the unwrapped value from its lazy wrapper. - * - * @private - * @name value - * @memberOf LazyWrapper - * @returns {*} Returns the unwrapped value. - */ - function lazyValue() { - var array = this.__wrapped__.value(), - dir = this.__dir__, - isArr = isArray(array), - isRight = dir < 0, - arrLength = isArr ? array.length : 0, - view = getView(0, arrLength, this.__views__), - start = view.start, - end = view.end, - length = end - start, - index = isRight ? end : (start - 1), - iteratees = this.__iteratees__, - iterLength = iteratees.length, - resIndex = 0, - takeCount = nativeMin(length, this.__takeCount__); - - if (!isArr || arrLength < LARGE_ARRAY_SIZE || (arrLength == length && takeCount == length)) { - return baseWrapperValue(array, this.__actions__); - } - var result = []; - - outer: - while (length-- && resIndex < takeCount) { - index += dir; - - var iterIndex = -1, - value = array[index]; - - while (++iterIndex < iterLength) { - var data = iteratees[iterIndex], - iteratee = data.iteratee, - type = data.type, - computed = iteratee(value); - - if (type == LAZY_MAP_FLAG) { - value = computed; - } else if (!computed) { - if (type == LAZY_FILTER_FLAG) { - continue outer; - } else { - break outer; - } - } - } - result[resIndex++] = value; - } - return result; - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates an hash object. - * - * @private - * @returns {Object} Returns the new hash object. - */ - function Hash() {} - - /** - * Removes `key` and its value from the hash. - * - * @private - * @param {Object} hash The hash to modify. - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function hashDelete(hash, key) { - return hashHas(hash, key) && delete hash[key]; - } - - /** - * Gets the hash value for `key`. - * - * @private - * @param {Object} hash The hash to query. - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function hashGet(hash, key) { - if (nativeCreate) { - var result = hash[key]; - return result === HASH_UNDEFINED ? undefined : result; - } - return hasOwnProperty.call(hash, key) ? hash[key] : undefined; - } - - /** - * Checks if a hash value for `key` exists. - * - * @private - * @param {Object} hash The hash to query. - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function hashHas(hash, key) { - return nativeCreate ? hash[key] !== undefined : hasOwnProperty.call(hash, key); - } - - /** - * Sets the hash `key` to `value`. - * - * @private - * @param {Object} hash The hash to modify. - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - */ - function hashSet(hash, key, value) { - hash[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates a map cache object to store key-value pairs. - * - * @private - * @param {Array} [values] The values to cache. - */ - function MapCache(values) { - var index = -1, - length = values ? values.length : 0; - - this.clear(); - while (++index < length) { - var entry = values[index]; - this.set(entry[0], entry[1]); - } - } - - /** - * Removes all key-value entries from the map. - * - * @private - * @name clear - * @memberOf MapCache - */ - function mapClear() { - this.__data__ = { 'hash': new Hash, 'map': Map ? new Map : [], 'string': new Hash }; - } - - /** - * Removes `key` and its value from the map. - * - * @private - * @name delete - * @memberOf MapCache - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function mapDelete(key) { - var data = this.__data__; - if (isKeyable(key)) { - return hashDelete(typeof key == 'string' ? data.string : data.hash, key); - } - return Map ? data.map['delete'](key) : assocDelete(data.map, key); - } - - /** - * Gets the map value for `key`. - * - * @private - * @name get - * @memberOf MapCache - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function mapGet(key) { - var data = this.__data__; - if (isKeyable(key)) { - return hashGet(typeof key == 'string' ? data.string : data.hash, key); - } - return Map ? data.map.get(key) : assocGet(data.map, key); - } - - /** - * Checks if a map value for `key` exists. - * - * @private - * @name has - * @memberOf MapCache - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function mapHas(key) { - var data = this.__data__; - if (isKeyable(key)) { - return hashHas(typeof key == 'string' ? data.string : data.hash, key); - } - return Map ? data.map.has(key) : assocHas(data.map, key); - } - - /** - * Sets the map `key` to `value`. - * - * @private - * @name set - * @memberOf MapCache - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - * @returns {Object} Returns the map cache object. - */ - function mapSet(key, value) { - var data = this.__data__; - if (isKeyable(key)) { - hashSet(typeof key == 'string' ? data.string : data.hash, key, value); - } else if (Map) { - data.map.set(key, value); - } else { - assocSet(data.map, key, value); - } - return this; - } - - /*------------------------------------------------------------------------*/ - - /** - * - * Creates a set cache object to store unique values. - * - * @private - * @param {Array} [values] The values to cache. - */ - function SetCache(values) { - var index = -1, - length = values ? values.length : 0; - - this.__data__ = new MapCache; - while (++index < length) { - this.push(values[index]); - } - } - - /** - * Checks if `value` is in `cache`. - * - * @private - * @param {Object} cache The set cache to search. - * @param {*} value The value to search for. - * @returns {number} Returns `true` if `value` is found, else `false`. - */ - function cacheHas(cache, value) { - var map = cache.__data__; - if (isKeyable(value)) { - var data = map.__data__, - hash = typeof value == 'string' ? data.string : data.hash; - - return hash[value] === HASH_UNDEFINED; - } - return map.has(value); - } - - /** - * Adds `value` to the set cache. - * - * @private - * @name push - * @memberOf SetCache - * @param {*} value The value to cache. - */ - function cachePush(value) { - var map = this.__data__; - if (isKeyable(value)) { - var data = map.__data__, - hash = typeof value == 'string' ? data.string : data.hash; - - hash[value] = HASH_UNDEFINED; - } - else { - map.set(value, HASH_UNDEFINED); - } - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates a stack cache object to store key-value pairs. - * - * @private - * @param {Array} [values] The values to cache. - */ - function Stack(values) { - var index = -1, - length = values ? values.length : 0; - - this.clear(); - while (++index < length) { - var entry = values[index]; - this.set(entry[0], entry[1]); - } - } - - /** - * Removes all key-value entries from the stack. - * - * @private - * @name clear - * @memberOf Stack - */ - function stackClear() { - this.__data__ = { 'array': [], 'map': null }; - } - - /** - * Removes `key` and its value from the stack. - * - * @private - * @name delete - * @memberOf Stack - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function stackDelete(key) { - var data = this.__data__, - array = data.array; - - return array ? assocDelete(array, key) : data.map['delete'](key); - } - - /** - * Gets the stack value for `key`. - * - * @private - * @name get - * @memberOf Stack - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function stackGet(key) { - var data = this.__data__, - array = data.array; - - return array ? assocGet(array, key) : data.map.get(key); - } - - /** - * Checks if a stack value for `key` exists. - * - * @private - * @name has - * @memberOf Stack - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function stackHas(key) { - var data = this.__data__, - array = data.array; - - return array ? assocHas(array, key) : data.map.has(key); - } - - /** - * Sets the stack `key` to `value`. - * - * @private - * @name set - * @memberOf Stack - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - * @returns {Object} Returns the stack cache object. - */ - function stackSet(key, value) { - var data = this.__data__, - array = data.array; - - if (array) { - if (array.length < (LARGE_ARRAY_SIZE - 1)) { - assocSet(array, key, value); - } else { - data.array = null; - data.map = new MapCache(array); - } - } - var map = data.map; - if (map) { - map.set(key, value); - } - return this; - } - - /*------------------------------------------------------------------------*/ - - /** - * Removes `key` and its value from the associative array. - * - * @private - * @param {Array} array The array to query. - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function assocDelete(array, key) { - var index = assocIndexOf(array, key); - if (index < 0) { - return false; - } - var lastIndex = array.length - 1; - if (index == lastIndex) { - array.pop(); - } else { - splice.call(array, index, 1); - } - return true; - } - - /** - * Gets the associative array value for `key`. - * - * @private - * @param {Array} array The array to query. - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function assocGet(array, key) { - var index = assocIndexOf(array, key); - return index < 0 ? undefined : array[index][1]; - } - - /** - * Checks if an associative array value for `key` exists. - * - * @private - * @param {Array} array The array to query. - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function assocHas(array, key) { - return assocIndexOf(array, key) > -1; - } - - /** - * Gets the index at which the first occurrence of `key` is found in `array` - * of key-value pairs. - * - * @private - * @param {Array} array The array to search. - * @param {*} key The key to search for. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function assocIndexOf(array, key) { - var length = array.length; - while (length--) { - if (eq(array[length][0], key)) { - return length; - } - } - return -1; - } - - /** - * Sets the associative array `key` to `value`. - * - * @private - * @param {Array} array The array to modify. - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - */ - function assocSet(array, key, value) { - var index = assocIndexOf(array, key); - if (index < 0) { - array.push([key, value]); - } else { - array[index][1] = value; - } - } - - /*------------------------------------------------------------------------*/ - - /** - * Used by `_.defaults` to customize its `_.assignIn` use. - * - * @private - * @param {*} objValue The destination value. - * @param {*} srcValue The source value. - * @param {string} key The key of the property to assign. - * @param {Object} object The parent object of `objValue`. - * @returns {*} Returns the value to assign. - */ - function assignInDefaults(objValue, srcValue, key, object) { - if (objValue === undefined || - (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) { - return srcValue; - } - return objValue; - } - - /** - * This function is like `assignValue` except that it doesn't assign `undefined` values. - * - * @private - * @param {Object} object The object to modify. - * @param {string} key The key of the property to assign. - * @param {*} value The value to assign. - */ - function assignMergeValue(object, key, value) { - if ((value !== undefined && !eq(object[key], value)) || - (typeof key == 'number' && value === undefined && !(key in object))) { - object[key] = value; - } - } - - /** - * Assigns `value` to `key` of `object` if the existing value is not equivalent - * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. - * - * @private - * @param {Object} object The object to modify. - * @param {string} key The key of the property to assign. - * @param {*} value The value to assign. - */ - function assignValue(object, key, value) { - var objValue = object[key]; - if ((!eq(objValue, value) || - (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) || - (value === undefined && !(key in object))) { - object[key] = value; - } - } - - /** - * The base implementation of `_.assign` without support for multiple sources - * or `customizer` functions. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @returns {Object} Returns `object`. - */ - function baseAssign(object, source) { - return object && copyObject(source, keys(source), object); - } - - /** - * The base implementation of `_.at` without support for individual paths. - * - * @private - * @param {Object} object The object to iterate over. - * @param {string[]} paths The property paths of elements to pick. - * @returns {Array} Returns the new array of picked elements. - */ - function baseAt(object, paths) { - var index = -1, - isNil = object == null, - length = paths.length, - result = Array(length); - - while (++index < length) { - result[index] = isNil ? undefined : get(object, paths[index]); - } - return result; - } - - /** - * The base implementation of `_.clamp` which doesn't coerce arguments to numbers. - * - * @private - * @param {number} number The number to clamp. - * @param {number} [lower] The lower bound. - * @param {number} upper The upper bound. - * @returns {number} Returns the clamped number. - */ - function baseClamp(number, lower, upper) { - if (number === number) { - if (upper !== undefined) { - number = number <= upper ? number : upper; - } - if (lower !== undefined) { - number = number >= lower ? number : lower; - } - } - return number; - } - - /** - * The base implementation of `_.clone` and `_.cloneDeep` which tracks - * traversed objects. - * - * @private - * @param {*} value The value to clone. - * @param {boolean} [isDeep] Specify a deep clone. - * @param {Function} [customizer] The function to customize cloning. - * @param {string} [key] The key of `value`. - * @param {Object} [object] The parent object of `value`. - * @param {Object} [stack] Tracks traversed objects and their clone counterparts. - * @returns {*} Returns the cloned value. - */ - function baseClone(value, isDeep, customizer, key, object, stack) { - var result; - if (customizer) { - result = object ? customizer(value, key, object, stack) : customizer(value); - } - if (result !== undefined) { - return result; - } - if (!isObject(value)) { - return value; - } - var isArr = isArray(value); - if (isArr) { - result = initCloneArray(value); - if (!isDeep) { - return copyArray(value, result); - } - } else { - var tag = getTag(value), - isFunc = tag == funcTag || tag == genTag; - - if (tag == objectTag || tag == argsTag || (isFunc && !object)) { - if (isHostObject(value)) { - return object ? value : {}; - } - result = initCloneObject(isFunc ? {} : value); - if (!isDeep) { - return copySymbols(value, baseAssign(result, value)); - } - } else { - return cloneableTags[tag] - ? initCloneByTag(value, tag, isDeep) - : (object ? value : {}); - } - } - // Check for circular references and return its corresponding clone. - stack || (stack = new Stack); - var stacked = stack.get(value); - if (stacked) { - return stacked; - } - stack.set(value, result); - - // Recursively populate clone (susceptible to call stack limits). - (isArr ? arrayEach : baseForOwn)(value, function(subValue, key) { - assignValue(result, key, baseClone(subValue, isDeep, customizer, key, value, stack)); - }); - return isArr ? result : copySymbols(value, result); - } - - /** - * The base implementation of `_.conforms` which doesn't clone `source`. - * - * @private - * @param {Object} source The object of property predicates to conform to. - * @returns {Function} Returns the new function. - */ - function baseConforms(source) { - var props = keys(source), - length = props.length; - - return function(object) { - if (object == null) { - return !length; - } - var index = length; - while (index--) { - var key = props[index], - predicate = source[key], - value = object[key]; - - if ((value === undefined && !(key in Object(object))) || !predicate(value)) { - return false; - } - } - return true; - }; - } - - /** - * The base implementation of `_.create` without support for assigning - * properties to the created object. - * - * @private - * @param {Object} prototype The object to inherit from. - * @returns {Object} Returns the new object. - */ - var baseCreate = (function() { - function object() {} - return function(prototype) { - if (isObject(prototype)) { - object.prototype = prototype; - var result = new object; - object.prototype = undefined; - } - return result || {}; - }; - }()); - - /** - * The base implementation of `_.delay` and `_.defer` which accepts an array - * of `func` arguments. - * - * @private - * @param {Function} func The function to delay. - * @param {number} wait The number of milliseconds to delay invocation. - * @param {Object} args The arguments provide to `func`. - * @returns {number} Returns the timer id. - */ - function baseDelay(func, wait, args) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - return setTimeout(function() { func.apply(undefined, args); }, wait); - } - - /** - * The base implementation of methods like `_.difference` without support for - * excluding multiple arrays or iteratee shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Array} values The values to exclude. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of filtered values. - */ - function baseDifference(array, values, iteratee, comparator) { - var index = -1, - includes = arrayIncludes, - isCommon = true, - length = array.length, - result = [], - valuesLength = values.length; - - if (!length) { - return result; - } - if (iteratee) { - values = arrayMap(values, baseUnary(iteratee)); - } - if (comparator) { - includes = arrayIncludesWith; - isCommon = false; - } - else if (values.length >= LARGE_ARRAY_SIZE) { - includes = cacheHas; - isCommon = false; - values = new SetCache(values); - } - outer: - while (++index < length) { - var value = array[index], - computed = iteratee ? iteratee(value) : value; - - if (isCommon && computed === computed) { - var valuesIndex = valuesLength; - while (valuesIndex--) { - if (values[valuesIndex] === computed) { - continue outer; - } - } - result.push(value); - } - else if (!includes(values, computed, comparator)) { - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.forEach` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - */ - var baseEach = createBaseEach(baseForOwn); - - /** - * The base implementation of `_.forEachRight` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - */ - var baseEachRight = createBaseEach(baseForOwnRight, true); - - /** - * The base implementation of `_.every` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if all elements pass the predicate check, else `false` - */ - function baseEvery(collection, predicate) { - var result = true; - baseEach(collection, function(value, index, collection) { - result = !!predicate(value, index, collection); - return result; - }); - return result; - } - - /** - * The base implementation of `_.fill` without an iteratee call guard. - * - * @private - * @param {Array} array The array to fill. - * @param {*} value The value to fill `array` with. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns `array`. - */ - function baseFill(array, value, start, end) { - var length = array.length; - - start = toInteger(start); - if (start < 0) { - start = -start > length ? 0 : (length + start); - } - end = (end === undefined || end > length) ? length : toInteger(end); - if (end < 0) { - end += length; - } - end = start > end ? 0 : toLength(end); - while (start < end) { - array[start++] = value; - } - return array; - } - - /** - * The base implementation of `_.filter` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - */ - function baseFilter(collection, predicate) { - var result = []; - baseEach(collection, function(value, index, collection) { - if (predicate(value, index, collection)) { - result.push(value); - } - }); - return result; - } - - /** - * The base implementation of `_.flatten` with support for restricting flattening. - * - * @private - * @param {Array} array The array to flatten. - * @param {boolean} [isDeep] Specify a deep flatten. - * @param {boolean} [isStrict] Restrict flattening to arrays-like objects. - * @param {Array} [result=[]] The initial result value. - * @returns {Array} Returns the new flattened array. - */ - function baseFlatten(array, isDeep, isStrict, result) { - result || (result = []); - - var index = -1, - length = array.length; - - while (++index < length) { - var value = array[index]; - if (isArrayLikeObject(value) && - (isStrict || isArray(value) || isArguments(value))) { - if (isDeep) { - // Recursively flatten arrays (susceptible to call stack limits). - baseFlatten(value, isDeep, isStrict, result); - } else { - arrayPush(result, value); - } - } else if (!isStrict) { - result[result.length] = value; - } - } - return result; - } - - /** - * The base implementation of `baseForIn` and `baseForOwn` which iterates - * over `object` properties returned by `keysFunc` invoking `iteratee` for - * each property. Iteratee functions may exit iteration early by explicitly - * returning `false`. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {Function} keysFunc The function to get the keys of `object`. - * @returns {Object} Returns `object`. - */ - var baseFor = createBaseFor(); - - /** - * This function is like `baseFor` except that it iterates over properties - * in the opposite order. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {Function} keysFunc The function to get the keys of `object`. - * @returns {Object} Returns `object`. - */ - var baseForRight = createBaseFor(true); - - /** - * The base implementation of `_.forIn` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Object} Returns `object`. - */ - function baseForIn(object, iteratee) { - return object == null ? object : baseFor(object, iteratee, keysIn); - } - - /** - * The base implementation of `_.forOwn` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Object} Returns `object`. - */ - function baseForOwn(object, iteratee) { - return object && baseFor(object, iteratee, keys); - } - - /** - * The base implementation of `_.forOwnRight` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Object} Returns `object`. - */ - function baseForOwnRight(object, iteratee) { - return object && baseForRight(object, iteratee, keys); - } - - /** - * The base implementation of `_.functions` which creates an array of - * `object` function property names filtered from those provided. - * - * @private - * @param {Object} object The object to inspect. - * @param {Array} props The property names to filter. - * @returns {Array} Returns the new array of filtered property names. - */ - function baseFunctions(object, props) { - return arrayFilter(props, function(key) { - return isFunction(object[key]); - }); - } - - /** - * The base implementation of `_.get` without support for default values. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to get. - * @returns {*} Returns the resolved value. - */ - function baseGet(object, path) { - path = isKey(path, object) ? [path + ''] : baseToPath(path); - - var index = 0, - length = path.length; - - while (object != null && index < length) { - object = object[path[index++]]; - } - return (index && index == length) ? object : undefined; - } - - /** - * The base implementation of `_.has` without support for deep paths. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} key The key to check. - * @returns {boolean} Returns `true` if `key` exists, else `false`. - */ - function baseHas(object, key) { - // Avoid a bug in IE 10-11 where objects with a [[Prototype]] of `null`, - // that are composed entirely of index properties, return `false` for - // `hasOwnProperty` checks of them. - return hasOwnProperty.call(object, key) || - (typeof object == 'object' && key in object && getPrototypeOf(object) === null); - } - - /** - * The base implementation of `_.hasIn` without support for deep paths. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} key The key to check. - * @returns {boolean} Returns `true` if `key` exists, else `false`. - */ - function baseHasIn(object, key) { - return key in Object(object); - } - - /** - * The base implementation of `_.inRange` which doesn't coerce arguments to numbers. - * - * @private - * @param {number} number The number to check. - * @param {number} start The start of the range. - * @param {number} end The end of the range. - * @returns {boolean} Returns `true` if `number` is in the range, else `false`. - */ - function baseInRange(number, start, end) { - return number >= nativeMin(start, end) && number < nativeMax(start, end); - } - - /** - * The base implementation of methods like `_.intersection`, without support - * for iteratee shorthands, that accepts an array of arrays to inspect. - * - * @private - * @param {Array} arrays The arrays to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of shared values. - */ - function baseIntersection(arrays, iteratee, comparator) { - var includes = comparator ? arrayIncludesWith : arrayIncludes, - othLength = arrays.length, - othIndex = othLength, - caches = Array(othLength), - result = []; - - while (othIndex--) { - var array = arrays[othIndex]; - if (othIndex && iteratee) { - array = arrayMap(array, baseUnary(iteratee)); - } - caches[othIndex] = !comparator && (iteratee || array.length >= 120) - ? new SetCache(othIndex && array) - : undefined; - } - array = arrays[0]; - - var index = -1, - length = array.length, - seen = caches[0]; - - outer: - while (++index < length) { - var value = array[index], - computed = iteratee ? iteratee(value) : value; - - if (!(seen ? cacheHas(seen, computed) : includes(result, computed, comparator))) { - var othIndex = othLength; - while (--othIndex) { - var cache = caches[othIndex]; - if (!(cache ? cacheHas(cache, computed) : includes(arrays[othIndex], computed, comparator))) { - continue outer; - } - } - if (seen) { - seen.push(computed); - } - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.invoke` without support for individual - * method arguments. - * - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path of the method to invoke. - * @param {Array} args The arguments to invoke the method with. - * @returns {*} Returns the result of the invoked method. - */ - function baseInvoke(object, path, args) { - if (!isKey(path, object)) { - path = baseToPath(path); - object = parent(object, path); - path = last(path); - } - var func = object == null ? object : object[path]; - return func == null ? undefined : apply(func, object, args); - } - - /** - * The base implementation of `_.isEqual` which supports partial comparisons - * and tracks traversed objects. - * - * @private - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @param {Function} [customizer] The function to customize comparisons. - * @param {boolean} [bitmask] The bitmask of comparison flags. - * The bitmask may be composed of the following flags: - * 1 - Unordered comparison - * 2 - Partial comparison - * @param {Object} [stack] Tracks traversed `value` and `other` objects. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - */ - function baseIsEqual(value, other, customizer, bitmask, stack) { - if (value === other) { - return true; - } - if (value == null || other == null || (!isObject(value) && !isObjectLike(other))) { - return value !== value && other !== other; - } - return baseIsEqualDeep(value, other, baseIsEqual, customizer, bitmask, stack); - } - - /** - * A specialized version of `baseIsEqual` for arrays and objects which performs - * deep comparisons and tracks traversed objects enabling objects with circular - * references to be compared. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Function} [customizer] The function to customize comparisons. - * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual` for more details. - * @param {Object} [stack] Tracks traversed `object` and `other` objects. - * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. - */ - function baseIsEqualDeep(object, other, equalFunc, customizer, bitmask, stack) { - var objIsArr = isArray(object), - othIsArr = isArray(other), - objTag = arrayTag, - othTag = arrayTag; - - if (!objIsArr) { - objTag = getTag(object); - if (objTag == argsTag) { - objTag = objectTag; - } else if (objTag != objectTag) { - objIsArr = isTypedArray(object); - } - } - if (!othIsArr) { - othTag = getTag(other); - if (othTag == argsTag) { - othTag = objectTag; - } else if (othTag != objectTag) { - othIsArr = isTypedArray(other); - } - } - var objIsObj = objTag == objectTag && !isHostObject(object), - othIsObj = othTag == objectTag && !isHostObject(other), - isSameTag = objTag == othTag; - - if (isSameTag && !(objIsArr || objIsObj)) { - return equalByTag(object, other, objTag, equalFunc, customizer, bitmask); - } - var isPartial = bitmask & PARTIAL_COMPARE_FLAG; - if (!isPartial) { - var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), - othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); - - if (objIsWrapped || othIsWrapped) { - return equalFunc(objIsWrapped ? object.value() : object, othIsWrapped ? other.value() : other, customizer, bitmask, stack); - } - } - if (!isSameTag) { - return false; - } - stack || (stack = new Stack); - return (objIsArr ? equalArrays : equalObjects)(object, other, equalFunc, customizer, bitmask, stack); - } - - /** - * The base implementation of `_.isMatch` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to inspect. - * @param {Object} source The object of property values to match. - * @param {Array} matchData The property names, values, and compare flags to match. - * @param {Function} [customizer] The function to customize comparisons. - * @returns {boolean} Returns `true` if `object` is a match, else `false`. - */ - function baseIsMatch(object, source, matchData, customizer) { - var index = matchData.length, - length = index, - noCustomizer = !customizer; - - if (object == null) { - return !length; - } - object = Object(object); - while (index--) { - var data = matchData[index]; - if ((noCustomizer && data[2]) - ? data[1] !== object[data[0]] - : !(data[0] in object) - ) { - return false; - } - } - while (++index < length) { - data = matchData[index]; - var key = data[0], - objValue = object[key], - srcValue = data[1]; - - if (noCustomizer && data[2]) { - if (objValue === undefined && !(key in object)) { - return false; - } - } else { - var stack = new Stack, - result = customizer ? customizer(objValue, srcValue, key, object, source, stack) : undefined; - - if (!(result === undefined ? baseIsEqual(srcValue, objValue, customizer, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG, stack) : result)) { - return false; - } - } - } - return true; - } - - /** - * The base implementation of `_.iteratee`. - * - * @private - * @param {*} [value=_.identity] The value to convert to an iteratee. - * @returns {Function} Returns the iteratee. - */ - function baseIteratee(value) { - var type = typeof value; - if (type == 'function') { - return value; - } - if (value == null) { - return identity; - } - if (type == 'object') { - return isArray(value) - ? baseMatchesProperty(value[0], value[1]) - : baseMatches(value); - } - return property(value); - } - - /** - * The base implementation of `_.keys` which doesn't skip the constructor - * property of prototypes or treat sparse arrays as dense. - * - * @private - * @type Function - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - */ - function baseKeys(object) { - return nativeKeys(Object(object)); - } - - /** - * The base implementation of `_.keysIn` which doesn't skip the constructor - * property of prototypes or treat sparse arrays as dense. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - */ - function baseKeysIn(object) { - object = object == null ? object : Object(object); - - var result = []; - for (var key in object) { - result.push(key); - } - return result; - } - - // Fallback for IE < 9 with es6-shim. - if (enumerate && !propertyIsEnumerable.call({ 'valueOf': 1 }, 'valueOf')) { - baseKeysIn = function(object) { - return iteratorToArray(enumerate(object)); - }; - } - - /** - * The base implementation of `_.map` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - */ - function baseMap(collection, iteratee) { - var index = -1, - result = isArrayLike(collection) ? Array(collection.length) : []; - - baseEach(collection, function(value, key, collection) { - result[++index] = iteratee(value, key, collection); - }); - return result; - } - - /** - * The base implementation of `_.matches` which doesn't clone `source`. - * - * @private - * @param {Object} source The object of property values to match. - * @returns {Function} Returns the new function. - */ - function baseMatches(source) { - var matchData = getMatchData(source); - if (matchData.length == 1 && matchData[0][2]) { - var key = matchData[0][0], - value = matchData[0][1]; - - return function(object) { - if (object == null) { - return false; - } - return object[key] === value && - (value !== undefined || (key in Object(object))); - }; - } - return function(object) { - return object === source || baseIsMatch(object, source, matchData); - }; - } - - /** - * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. - * - * @private - * @param {string} path The path of the property to get. - * @param {*} srcValue The value to match. - * @returns {Function} Returns the new function. - */ - function baseMatchesProperty(path, srcValue) { - return function(object) { - var objValue = get(object, path); - return (objValue === undefined && objValue === srcValue) - ? hasIn(object, path) - : baseIsEqual(srcValue, objValue, undefined, UNORDERED_COMPARE_FLAG | PARTIAL_COMPARE_FLAG); - }; - } - - /** - * The base implementation of `_.merge` without support for multiple sources. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @param {Function} [customizer] The function to customize merged values. - * @param {Object} [stack] Tracks traversed source values and their merged counterparts. - */ - function baseMerge(object, source, customizer, stack) { - if (object === source) { - return; - } - var props = (isArray(source) || isTypedArray(source)) ? undefined : keysIn(source); - arrayEach(props || source, function(srcValue, key) { - if (props) { - key = srcValue; - srcValue = source[key]; - } - if (isObject(srcValue)) { - stack || (stack = new Stack); - baseMergeDeep(object, source, key, baseMerge, customizer, stack); - } - else { - var newValue = customizer ? customizer(object[key], srcValue, (key + ''), object, source, stack) : undefined; - if (newValue === undefined) { - newValue = srcValue; - } - assignMergeValue(object, key, newValue); - } - }); - } - - /** - * A specialized version of `baseMerge` for arrays and objects which performs - * deep merges and tracks traversed objects enabling objects with circular - * references to be merged. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @param {string} key The key of the value to merge. - * @param {Function} mergeFunc The function to merge values. - * @param {Function} [customizer] The function to customize assigned values. - * @param {Object} [stack] Tracks traversed source values and their merged counterparts. - */ - function baseMergeDeep(object, source, key, mergeFunc, customizer, stack) { - var objValue = object[key], - srcValue = source[key], - stacked = stack.get(srcValue) || stack.get(objValue); - - if (stacked) { - assignMergeValue(object, key, stacked); - return; - } - var newValue = customizer ? customizer(objValue, srcValue, (key + ''), object, source, stack) : undefined, - isCommon = newValue === undefined; - - if (isCommon) { - newValue = srcValue; - if (isArray(srcValue) || isTypedArray(srcValue)) { - newValue = isArray(objValue) - ? objValue - : ((isArrayLikeObject(objValue)) ? copyArray(objValue) : baseClone(srcValue)); - } - else if (isPlainObject(srcValue) || isArguments(srcValue)) { - newValue = isArguments(objValue) - ? toPlainObject(objValue) - : (isObject(objValue) ? objValue : baseClone(srcValue)); - } - else { - isCommon = isFunction(srcValue); - } - } - stack.set(srcValue, newValue); - - if (isCommon) { - // Recursively merge objects and arrays (susceptible to call stack limits). - mergeFunc(newValue, srcValue, customizer, stack); - } - assignMergeValue(object, key, newValue); - } - - /** - * The base implementation of `_.orderBy` without param guards. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. - * @param {string[]} orders The sort orders of `iteratees`. - * @returns {Array} Returns the new sorted array. - */ - function baseOrderBy(collection, iteratees, orders) { - var index = -1, - toIteratee = getIteratee(); - - iteratees = arrayMap(iteratees.length ? iteratees : Array(1), function(iteratee) { - return toIteratee(iteratee); - }); - - var result = baseMap(collection, function(value, key, collection) { - var criteria = arrayMap(iteratees, function(iteratee) { - return iteratee(value); - }); - return { 'criteria': criteria, 'index': ++index, 'value': value }; - }); - - return baseSortBy(result, function(object, other) { - return compareMultiple(object, other, orders); - }); - } - - /** - * The base implementation of `_.pick` without support for individual - * property names. - * - * @private - * @param {Object} object The source object. - * @param {string[]} props The property names to pick. - * @returns {Object} Returns the new object. - */ - function basePick(object, props) { - object = Object(object); - return arrayReduce(props, function(result, key) { - if (key in object) { - result[key] = object[key]; - } - return result; - }, {}); - } - - /** - * The base implementation of `_.pickBy` without support for iteratee shorthands. - * - * @private - * @param {Object} object The source object. - * @param {Function} predicate The function invoked per property. - * @returns {Object} Returns the new object. - */ - function basePickBy(object, predicate) { - var result = {}; - baseForIn(object, function(value, key) { - if (predicate(value)) { - result[key] = value; - } - }); - return result; - } - - /** - * The base implementation of `_.property` without support for deep paths. - * - * @private - * @param {string} key The key of the property to get. - * @returns {Function} Returns the new function. - */ - function baseProperty(key) { - return function(object) { - return object == null ? undefined : object[key]; - }; - } - - /** - * A specialized version of `baseProperty` which supports deep paths. - * - * @private - * @param {Array|string} path The path of the property to get. - * @returns {Function} Returns the new function. - */ - function basePropertyDeep(path) { - return function(object) { - return baseGet(object, path); - }; - } - - /** - * The base implementation of `_.pullAll`. - * - * @private - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @returns {Array} Returns `array`. - */ - function basePullAll(array, values) { - return basePullAllBy(array, values); - } - - /** - * The base implementation of `_.pullAllBy` without support for iteratee - * shorthands. - * - * @private - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @param {Function} [iteratee] The iteratee invoked per element. - * @returns {Array} Returns `array`. - */ - function basePullAllBy(array, values, iteratee) { - var index = -1, - length = values.length, - seen = array; - - if (iteratee) { - seen = arrayMap(array, function(value) { return iteratee(value); }); - } - while (++index < length) { - var fromIndex = 0, - value = values[index], - computed = iteratee ? iteratee(value) : value; - - while ((fromIndex = baseIndexOf(seen, computed, fromIndex)) > -1) { - if (seen !== array) { - splice.call(seen, fromIndex, 1); - } - splice.call(array, fromIndex, 1); - } - } - return array; - } - - /** - * The base implementation of `_.pullAt` without support for individual - * indexes or capturing the removed elements. - * - * @private - * @param {Array} array The array to modify. - * @param {number[]} indexes The indexes of elements to remove. - * @returns {Array} Returns `array`. - */ - function basePullAt(array, indexes) { - var length = array ? indexes.length : 0, - lastIndex = length - 1; - - while (length--) { - var index = indexes[length]; - if (lastIndex == length || index != previous) { - var previous = index; - if (isIndex(index)) { - splice.call(array, index, 1); - } - else if (!isKey(index, array)) { - var path = baseToPath(index), - object = parent(array, path); - - if (object != null) { - delete object[last(path)]; - } - } - else { - delete array[index]; - } - } - } - return array; - } - - /** - * The base implementation of `_.random` without support for returning - * floating-point numbers. - * - * @private - * @param {number} lower The lower bound. - * @param {number} upper The upper bound. - * @returns {number} Returns the random number. - */ - function baseRandom(lower, upper) { - return lower + nativeFloor(nativeRandom() * (upper - lower + 1)); - } - - /** - * The base implementation of `_.range` and `_.rangeRight` which doesn't - * coerce arguments to numbers. - * - * @private - * @param {number} start The start of the range. - * @param {number} end The end of the range. - * @param {number} step The value to increment or decrement by. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Array} Returns the new array of numbers. - */ - function baseRange(start, end, step, fromRight) { - var index = -1, - length = nativeMax(nativeCeil((end - start) / (step || 1)), 0), - result = Array(length); - - while (length--) { - result[fromRight ? length : ++index] = start; - start += step; - } - return result; - } - - /** - * The base implementation of `_.set`. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to set. - * @param {*} value The value to set. - * @param {Function} [customizer] The function to customize path creation. - * @returns {Object} Returns `object`. - */ - function baseSet(object, path, value, customizer) { - path = isKey(path, object) ? [path + ''] : baseToPath(path); - - var index = -1, - length = path.length, - lastIndex = length - 1, - nested = object; - - while (nested != null && ++index < length) { - var key = path[index]; - if (isObject(nested)) { - var newValue = value; - if (index != lastIndex) { - var objValue = nested[key]; - newValue = customizer ? customizer(objValue, key, nested) : undefined; - if (newValue === undefined) { - newValue = objValue == null ? (isIndex(path[index + 1]) ? [] : {}) : objValue; - } - } - assignValue(nested, key, newValue); - } - nested = nested[key]; - } - return object; - } - - /** - * The base implementation of `setData` without support for hot loop detection. - * - * @private - * @param {Function} func The function to associate metadata with. - * @param {*} data The metadata. - * @returns {Function} Returns `func`. - */ - var baseSetData = !metaMap ? identity : function(func, data) { - metaMap.set(func, data); - return func; - }; - - /** - * The base implementation of `_.slice` without an iteratee call guard. - * - * @private - * @param {Array} array The array to slice. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns the slice of `array`. - */ - function baseSlice(array, start, end) { - var index = -1, - length = array.length; - - if (start < 0) { - start = -start > length ? 0 : (length + start); - } - end = end > length ? length : end; - if (end < 0) { - end += length; - } - length = start > end ? 0 : ((end - start) >>> 0); - start >>>= 0; - - var result = Array(length); - while (++index < length) { - result[index] = array[index + start]; - } - return result; - } - - /** - * The base implementation of `_.some` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if any element passes the predicate check, else `false`. - */ - function baseSome(collection, predicate) { - var result; - - baseEach(collection, function(value, index, collection) { - result = predicate(value, index, collection); - return !result; - }); - return !!result; - } - - /** - * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which - * performs a binary search of `array` to determine the index at which `value` - * should be inserted into `array` in order to maintain its sort order. - * - * @private - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {boolean} [retHighest] Specify returning the highest qualified index. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - */ - function baseSortedIndex(array, value, retHighest) { - var low = 0, - high = array ? array.length : low; - - if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { - while (low < high) { - var mid = (low + high) >>> 1, - computed = array[mid]; - - if ((retHighest ? (computed <= value) : (computed < value)) && computed !== null) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - return baseSortedIndexBy(array, value, identity, retHighest); - } - - /** - * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy` - * which invokes `iteratee` for `value` and each element of `array` to compute - * their sort ranking. The iteratee is invoked with one argument; (value). - * - * @private - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {Function} iteratee The iteratee invoked per element. - * @param {boolean} [retHighest] Specify returning the highest qualified index. - * @returns {number} Returns the index at which `value` should be inserted into `array`. - */ - function baseSortedIndexBy(array, value, iteratee, retHighest) { - value = iteratee(value); - - var low = 0, - high = array ? array.length : 0, - valIsNaN = value !== value, - valIsNull = value === null, - valIsUndef = value === undefined; - - while (low < high) { - var mid = nativeFloor((low + high) / 2), - computed = iteratee(array[mid]), - isDef = computed !== undefined, - isReflexive = computed === computed; - - if (valIsNaN) { - var setLow = isReflexive || retHighest; - } else if (valIsNull) { - setLow = isReflexive && isDef && (retHighest || computed != null); - } else if (valIsUndef) { - setLow = isReflexive && (retHighest || isDef); - } else if (computed == null) { - setLow = false; - } else { - setLow = retHighest ? (computed <= value) : (computed < value); - } - if (setLow) { - low = mid + 1; - } else { - high = mid; - } - } - return nativeMin(high, MAX_ARRAY_INDEX); - } - - /** - * The base implementation of `_.sortedUniq`. - * - * @private - * @param {Array} array The array to inspect. - * @returns {Array} Returns the new duplicate free array. - */ - function baseSortedUniq(array) { - return baseSortedUniqBy(array); - } - - /** - * The base implementation of `_.sortedUniqBy` without support for iteratee - * shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @returns {Array} Returns the new duplicate free array. - */ - function baseSortedUniqBy(array, iteratee) { - var index = 0, - length = array.length, - value = array[0], - computed = iteratee ? iteratee(value) : value, - seen = computed, - resIndex = 0, - result = [value]; - - while (++index < length) { - value = array[index], - computed = iteratee ? iteratee(value) : value; - - if (!eq(computed, seen)) { - seen = computed; - result[++resIndex] = value; - } - } - return result; - } - - /** - * The base implementation of `_.toPath` which only converts `value` to a - * path if it's not one. - * - * @private - * @param {*} value The value to process. - * @returns {Array} Returns the property path array. - */ - function baseToPath(value) { - return isArray(value) ? value : stringToPath(value); - } - - /** - * The base implementation of `_.uniqBy` without support for iteratee shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new duplicate free array. - */ - function baseUniq(array, iteratee, comparator) { - var index = -1, - includes = arrayIncludes, - length = array.length, - isCommon = true, - result = [], - seen = result; - - if (comparator) { - isCommon = false; - includes = arrayIncludesWith; - } - else if (length >= LARGE_ARRAY_SIZE) { - var set = iteratee ? null : createSet(array); - if (set) { - return setToArray(set); - } - isCommon = false; - includes = cacheHas; - seen = new SetCache; - } - else { - seen = iteratee ? [] : result; - } - outer: - while (++index < length) { - var value = array[index], - computed = iteratee ? iteratee(value) : value; - - if (isCommon && computed === computed) { - var seenIndex = seen.length; - while (seenIndex--) { - if (seen[seenIndex] === computed) { - continue outer; - } - } - if (iteratee) { - seen.push(computed); - } - result.push(value); - } - else if (!includes(seen, computed, comparator)) { - if (seen !== result) { - seen.push(computed); - } - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.unset`. - * - * @private - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to unset. - * @returns {boolean} Returns `true` if the property is deleted, else `false`. - */ - function baseUnset(object, path) { - path = isKey(path, object) ? [path + ''] : baseToPath(path); - object = parent(object, path); - var key = last(path); - return (object != null && has(object, key)) ? delete object[key] : true; - } - - /** - * The base implementation of methods like `_.dropWhile` and `_.takeWhile` - * without support for iteratee shorthands. - * - * @private - * @param {Array} array The array to query. - * @param {Function} predicate The function invoked per iteration. - * @param {boolean} [isDrop] Specify dropping elements instead of taking them. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Array} Returns the slice of `array`. - */ - function baseWhile(array, predicate, isDrop, fromRight) { - var length = array.length, - index = fromRight ? length : -1; - - while ((fromRight ? index-- : ++index < length) && - predicate(array[index], index, array)) {} - - return isDrop - ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) - : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); - } - - /** - * The base implementation of `wrapperValue` which returns the result of - * performing a sequence of actions on the unwrapped `value`, where each - * successive action is supplied the return value of the previous. - * - * @private - * @param {*} value The unwrapped value. - * @param {Array} actions Actions to perform to resolve the unwrapped value. - * @returns {*} Returns the resolved value. - */ - function baseWrapperValue(value, actions) { - var result = value; - if (result instanceof LazyWrapper) { - result = result.value(); - } - return arrayReduce(actions, function(result, action) { - return action.func.apply(action.thisArg, arrayPush([result], action.args)); - }, result); - } - - /** - * The base implementation of methods like `_.xor`, without support for - * iteratee shorthands, that accepts an array of arrays to inspect. - * - * @private - * @param {Array} arrays The arrays to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of values. - */ - function baseXor(arrays, iteratee, comparator) { - var index = -1, - length = arrays.length; - - while (++index < length) { - var result = result - ? arrayPush( - baseDifference(result, arrays[index], iteratee, comparator), - baseDifference(arrays[index], result, iteratee, comparator) - ) - : arrays[index]; - } - return (result && result.length) ? baseUniq(result, iteratee, comparator) : []; - } - - /** - * Creates a clone of `buffer`. - * - * @private - * @param {ArrayBuffer} buffer The array buffer to clone. - * @returns {ArrayBuffer} Returns the cloned array buffer. - */ - function cloneBuffer(buffer) { - var Ctor = buffer.constructor, - result = new Ctor(buffer.byteLength), - view = new Uint8Array(result); - - view.set(new Uint8Array(buffer)); - return result; - } - - /** - * Creates a clone of `map`. - * - * @private - * @param {Object} map The map to clone. - * @returns {Object} Returns the cloned map. - */ - function cloneMap(map) { - var Ctor = map.constructor; - return arrayReduce(mapToArray(map), addMapEntry, new Ctor); - } - - /** - * Creates a clone of `regexp`. - * - * @private - * @param {Object} regexp The regexp to clone. - * @returns {Object} Returns the cloned regexp. - */ - function cloneRegExp(regexp) { - var Ctor = regexp.constructor, - result = new Ctor(regexp.source, reFlags.exec(regexp)); - - result.lastIndex = regexp.lastIndex; - return result; - } - - /** - * Creates a clone of `set`. - * - * @private - * @param {Object} set The set to clone. - * @returns {Object} Returns the cloned set. - */ - function cloneSet(set) { - var Ctor = set.constructor; - return arrayReduce(setToArray(set), addSetEntry, new Ctor); - } - - /** - * Creates a clone of the `symbol` object. - * - * @private - * @param {Object} symbol The symbol object to clone. - * @returns {Object} Returns the cloned symbol object. - */ - function cloneSymbol(symbol) { - return _Symbol ? Object(symbolValueOf.call(symbol)) : {}; - } - - /** - * Creates a clone of `typedArray`. - * - * @private - * @param {Object} typedArray The typed array to clone. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the cloned typed array. - */ - function cloneTypedArray(typedArray, isDeep) { - var buffer = typedArray.buffer, - Ctor = typedArray.constructor; - - return new Ctor(isDeep ? cloneBuffer(buffer) : buffer, typedArray.byteOffset, typedArray.length); - } - - /** - * Creates an array that is the composition of partially applied arguments, - * placeholders, and provided arguments into a single array of arguments. - * - * @private - * @param {Array|Object} args The provided arguments. - * @param {Array} partials The arguments to prepend to those provided. - * @param {Array} holders The `partials` placeholder indexes. - * @returns {Array} Returns the new array of composed arguments. - */ - function composeArgs(args, partials, holders) { - var holdersLength = holders.length, - argsIndex = -1, - argsLength = nativeMax(args.length - holdersLength, 0), - leftIndex = -1, - leftLength = partials.length, - result = Array(leftLength + argsLength); - - while (++leftIndex < leftLength) { - result[leftIndex] = partials[leftIndex]; - } - while (++argsIndex < holdersLength) { - result[holders[argsIndex]] = args[argsIndex]; - } - while (argsLength--) { - result[leftIndex++] = args[argsIndex++]; - } - return result; - } - - /** - * This function is like `composeArgs` except that the arguments composition - * is tailored for `_.partialRight`. - * - * @private - * @param {Array|Object} args The provided arguments. - * @param {Array} partials The arguments to append to those provided. - * @param {Array} holders The `partials` placeholder indexes. - * @returns {Array} Returns the new array of composed arguments. - */ - function composeArgsRight(args, partials, holders) { - var holdersIndex = -1, - holdersLength = holders.length, - argsIndex = -1, - argsLength = nativeMax(args.length - holdersLength, 0), - rightIndex = -1, - rightLength = partials.length, - result = Array(argsLength + rightLength); - - while (++argsIndex < argsLength) { - result[argsIndex] = args[argsIndex]; - } - var offset = argsIndex; - while (++rightIndex < rightLength) { - result[offset + rightIndex] = partials[rightIndex]; - } - while (++holdersIndex < holdersLength) { - result[offset + holders[holdersIndex]] = args[argsIndex++]; - } - return result; - } - - /** - * Copies the values of `source` to `array`. - * - * @private - * @param {Array} source The array to copy values from. - * @param {Array} [array=[]] The array to copy values to. - * @returns {Array} Returns `array`. - */ - function copyArray(source, array) { - var index = -1, - length = source.length; - - array || (array = Array(length)); - while (++index < length) { - array[index] = source[index]; - } - return array; - } - - /** - * Copies properties of `source` to `object`. - * - * @private - * @param {Object} source The object to copy properties from. - * @param {Array} props The property names to copy. - * @param {Object} [object={}] The object to copy properties to. - * @returns {Object} Returns `object`. - */ - function copyObject(source, props, object) { - return copyObjectWith(source, props, object); - } - - /** - * This function is like `copyObject` except that it accepts a function to - * customize copied values. - * - * @private - * @param {Object} source The object to copy properties from. - * @param {Array} props The property names to copy. - * @param {Object} [object={}] The object to copy properties to. - * @param {Function} [customizer] The function to customize copied values. - * @returns {Object} Returns `object`. - */ - function copyObjectWith(source, props, object, customizer) { - object || (object = {}); - - var index = -1, - length = props.length; - - while (++index < length) { - var key = props[index], - newValue = customizer ? customizer(object[key], source[key], key, object, source) : source[key]; - - assignValue(object, key, newValue); - } - return object; - } - - /** - * Copies own symbol properties of `source` to `object`. - * - * @private - * @param {Object} source The object to copy symbols from. - * @param {Object} [object={}] The object to copy symbols to. - * @returns {Object} Returns `object`. - */ - function copySymbols(source, object) { - return copyObject(source, getSymbols(source), object); - } - - /** - * Creates a function like `_.groupBy`. - * - * @private - * @param {Function} setter The function to set keys and values of the accumulator object. - * @param {Function} [initializer] The function to initialize the accumulator object. - * @returns {Function} Returns the new aggregator function. - */ - function createAggregator(setter, initializer) { - return function(collection, iteratee) { - var result = initializer ? initializer() : {}; - iteratee = getIteratee(iteratee); - - if (isArray(collection)) { - var index = -1, - length = collection.length; - - while (++index < length) { - var value = collection[index]; - setter(result, value, iteratee(value), collection); - } - } else { - baseEach(collection, function(value, key, collection) { - setter(result, value, iteratee(value), collection); - }); - } - return result; - }; - } - - /** - * Creates a function like `_.assign`. - * - * @private - * @param {Function} assigner The function to assign values. - * @returns {Function} Returns the new assigner function. - */ - function createAssigner(assigner) { - return rest(function(object, sources) { - var index = -1, - length = sources.length, - customizer = length > 1 ? sources[length - 1] : undefined, - guard = length > 2 ? sources[2] : undefined; - - customizer = typeof customizer == 'function' ? (length--, customizer) : undefined; - if (guard && isIterateeCall(sources[0], sources[1], guard)) { - customizer = length < 3 ? undefined : customizer; - length = 1; - } - object = Object(object); - while (++index < length) { - var source = sources[index]; - if (source) { - assigner(object, source, customizer); - } - } - return object; - }); - } - - /** - * Creates a `baseEach` or `baseEachRight` function. - * - * @private - * @param {Function} eachFunc The function to iterate over a collection. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new base function. - */ - function createBaseEach(eachFunc, fromRight) { - return function(collection, iteratee) { - if (collection == null) { - return collection; - } - if (!isArrayLike(collection)) { - return eachFunc(collection, iteratee); - } - var length = collection.length, - index = fromRight ? length : -1, - iterable = Object(collection); - - while ((fromRight ? index-- : ++index < length)) { - if (iteratee(iterable[index], index, iterable) === false) { - break; - } - } - return collection; - }; - } - - /** - * Creates a base function for methods like `_.forIn`. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new base function. - */ - function createBaseFor(fromRight) { - return function(object, iteratee, keysFunc) { - var index = -1, - iterable = Object(object), - props = keysFunc(object), - length = props.length; - - while (length--) { - var key = props[fromRight ? length : ++index]; - if (iteratee(iterable[key], key, iterable) === false) { - break; - } - } - return object; - }; - } - - /** - * Creates a function that wraps `func` to invoke it with the optional `this` - * binding of `thisArg`. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details. - * @param {*} [thisArg] The `this` binding of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createBaseWrapper(func, bitmask, thisArg) { - var isBind = bitmask & BIND_FLAG, - Ctor = createCtorWrapper(func); - - function wrapper() { - var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; - return fn.apply(isBind ? thisArg : this, arguments); - } - return wrapper; - } - - /** - * Creates a function like `_.lowerFirst`. - * - * @private - * @param {string} methodName The name of the `String` case method to use. - * @returns {Function} Returns the new function. - */ - function createCaseFirst(methodName) { - return function(string) { - string = toString(string); - - var strSymbols = reHasComplexSymbol.test(string) ? stringToArray(string) : undefined, - chr = strSymbols ? strSymbols[0] : string.charAt(0), - trailing = strSymbols ? strSymbols.slice(1).join('') : string.slice(1); - - return chr[methodName]() + trailing; - }; - } - - /** - * Creates a function like `_.camelCase`. - * - * @private - * @param {Function} callback The function to combine each word. - * @returns {Function} Returns the new compounder function. - */ - function createCompounder(callback) { - return function(string) { - return arrayReduce(words(deburr(string)), callback, ''); - }; - } - - /** - * Creates a function that produces an instance of `Ctor` regardless of - * whether it was invoked as part of a `new` expression or by `call` or `apply`. - * - * @private - * @param {Function} Ctor The constructor to wrap. - * @returns {Function} Returns the new wrapped function. - */ - function createCtorWrapper(Ctor) { - return function() { - // Use a `switch` statement to work with class constructors. - // See http://ecma-international.org/ecma-262/6.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist - // for more details. - var args = arguments; - switch (args.length) { - case 0: return new Ctor; - case 1: return new Ctor(args[0]); - case 2: return new Ctor(args[0], args[1]); - case 3: return new Ctor(args[0], args[1], args[2]); - case 4: return new Ctor(args[0], args[1], args[2], args[3]); - case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); - case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]); - case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - } - var thisBinding = baseCreate(Ctor.prototype), - result = Ctor.apply(thisBinding, args); - - // Mimic the constructor's `return` behavior. - // See https://es5.github.io/#x13.2.2 for more details. - return isObject(result) ? result : thisBinding; - }; - } - - /** - * Creates a function that wraps `func` to enable currying. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details. - * @param {number} arity The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createCurryWrapper(func, bitmask, arity) { - var Ctor = createCtorWrapper(func); - - function wrapper() { - var length = arguments.length, - index = length, - args = Array(length), - fn = (this && this !== root && this instanceof wrapper) ? Ctor : func, - placeholder = wrapper.placeholder; - - while (index--) { - args[index] = arguments[index]; - } - var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder) - ? [] - : replaceHolders(args, placeholder); - - length -= holders.length; - return length < arity - ? createRecurryWrapper(func, bitmask, createHybridWrapper, placeholder, undefined, args, holders, undefined, undefined, arity - length) - : apply(fn, this, args); - } - return wrapper; - } - - /** - * Creates a `_.flow` or `_.flowRight` function. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new flow function. - */ - function createFlow(fromRight) { - return rest(function(funcs) { - funcs = baseFlatten(funcs); - - var length = funcs.length, - index = length, - prereq = LodashWrapper.prototype.thru; - - if (fromRight) { - funcs.reverse(); - } - while (index--) { - var func = funcs[index]; - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - if (prereq && !wrapper && getFuncName(func) == 'wrapper') { - var wrapper = new LodashWrapper([], true); - } - } - index = wrapper ? index : length; - while (++index < length) { - func = funcs[index]; - - var funcName = getFuncName(func), - data = funcName == 'wrapper' ? getData(func) : undefined; - - if (data && isLaziable(data[0]) && data[1] == (ARY_FLAG | CURRY_FLAG | PARTIAL_FLAG | REARG_FLAG) && !data[4].length && data[9] == 1) { - wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); - } else { - wrapper = (func.length == 1 && isLaziable(func)) ? wrapper[funcName]() : wrapper.thru(func); - } - } - return function() { - var args = arguments, - value = args[0]; - - if (wrapper && args.length == 1 && isArray(value) && value.length >= LARGE_ARRAY_SIZE) { - return wrapper.plant(value).value(); - } - var index = 0, - result = length ? funcs[index].apply(this, args) : value; - - while (++index < length) { - result = funcs[index].call(this, result); - } - return result; - }; - }); - } - - /** - * Creates a function that wraps `func` to invoke it with optional `this` - * binding of `thisArg`, partial application, and currying. - * - * @private - * @param {Function|string} func The function or method name to wrap. - * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {Array} [partials] The arguments to prepend to those provided to the new function. - * @param {Array} [holders] The `partials` placeholder indexes. - * @param {Array} [partialsRight] The arguments to append to those provided to the new function. - * @param {Array} [holdersRight] The `partialsRight` placeholder indexes. - * @param {Array} [argPos] The argument positions of the new function. - * @param {number} [ary] The arity cap of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createHybridWrapper(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { - var isAry = bitmask & ARY_FLAG, - isBind = bitmask & BIND_FLAG, - isBindKey = bitmask & BIND_KEY_FLAG, - isCurry = bitmask & CURRY_FLAG, - isCurryRight = bitmask & CURRY_RIGHT_FLAG, - isFlip = bitmask & FLIP_FLAG, - Ctor = isBindKey ? undefined : createCtorWrapper(func); - - function wrapper() { - var length = arguments.length, - index = length, - args = Array(length); - - while (index--) { - args[index] = arguments[index]; - } - if (partials) { - args = composeArgs(args, partials, holders); - } - if (partialsRight) { - args = composeArgsRight(args, partialsRight, holdersRight); - } - if (isCurry || isCurryRight) { - var placeholder = wrapper.placeholder, - argsHolders = replaceHolders(args, placeholder); - - length -= argsHolders.length; - if (length < arity) { - return createRecurryWrapper(func, bitmask, createHybridWrapper, placeholder, thisArg, args, argsHolders, argPos, ary, arity - length); - } - } - var thisBinding = isBind ? thisArg : this, - fn = isBindKey ? thisBinding[func] : func; - - if (argPos) { - args = reorder(args, argPos); - } else if (isFlip && args.length > 1) { - args.reverse(); - } - if (isAry && ary < args.length) { - args.length = ary; - } - if (this && this !== root && this instanceof wrapper) { - fn = Ctor || createCtorWrapper(fn); - } - return fn.apply(thisBinding, args); - } - return wrapper; - } - - /** - * Creates a function like `_.over`. - * - * @private - * @param {Function} arrayFunc The function to iterate over iteratees. - * @returns {Function} Returns the new invoker function. - */ - function createOver(arrayFunc) { - return rest(function(iteratees) { - iteratees = arrayMap(baseFlatten(iteratees), getIteratee()); - return rest(function(args) { - var thisArg = this; - return arrayFunc(iteratees, function(iteratee) { - return apply(iteratee, thisArg, args); - }); - }); - }); - } - - /** - * Creates the padding for `string` based on `length`. The `chars` string - * is truncated if the number of characters exceeds `length`. - * - * @private - * @param {string} string The string to create padding for. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padding for `string`. - */ - function createPadding(string, length, chars) { - length = toInteger(length); - - var strLength = stringSize(string); - if (!length || strLength >= length) { - return ''; - } - var padLength = length - strLength; - chars = chars === undefined ? ' ' : (chars + ''); - - var result = repeat(chars, nativeCeil(padLength / stringSize(chars))); - return reHasComplexSymbol.test(chars) - ? stringToArray(result).slice(0, padLength).join('') - : result.slice(0, padLength); - } - - /** - * Creates a function that wraps `func` to invoke it with the optional `this` - * binding of `thisArg` and the `partials` prepended to those provided to - * the wrapper. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details. - * @param {*} thisArg The `this` binding of `func`. - * @param {Array} partials The arguments to prepend to those provided to the new function. - * @returns {Function} Returns the new wrapped function. - */ - function createPartialWrapper(func, bitmask, thisArg, partials) { - var isBind = bitmask & BIND_FLAG, - Ctor = createCtorWrapper(func); - - function wrapper() { - var argsIndex = -1, - argsLength = arguments.length, - leftIndex = -1, - leftLength = partials.length, - args = Array(leftLength + argsLength), - fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; - - while (++leftIndex < leftLength) { - args[leftIndex] = partials[leftIndex]; - } - while (argsLength--) { - args[leftIndex++] = arguments[++argsIndex]; - } - return apply(fn, isBind ? thisArg : this, args); - } - return wrapper; - } - - /** - * Creates a `_.range` or `_.rangeRight` function. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new range function. - */ - function createRange(fromRight) { - return function(start, end, step) { - if (step && typeof step != 'number' && isIterateeCall(start, end, step)) { - end = step = undefined; - } - // Ensure the sign of `-0` is preserved. - start = toNumber(start); - start = start === start ? start : 0; - if (end === undefined) { - end = start; - start = 0; - } else { - end = toNumber(end) || 0; - } - step = step === undefined ? (start < end ? 1 : -1) : (toNumber(step) || 0); - return baseRange(start, end, step, fromRight); - }; - } - - /** - * Creates a function that wraps `func` to continue currying. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask of wrapper flags. See `createWrapper` for more details. - * @param {Function} wrapFunc The function to create the `func` wrapper. - * @param {*} placeholder The placeholder to replace. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {Array} [partials] The arguments to prepend to those provided to the new function. - * @param {Array} [holders] The `partials` placeholder indexes. - * @param {Array} [argPos] The argument positions of the new function. - * @param {number} [ary] The arity cap of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createRecurryWrapper(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) { - var isCurry = bitmask & CURRY_FLAG, - newArgPos = argPos ? copyArray(argPos) : undefined, - newsHolders = isCurry ? holders : undefined, - newHoldersRight = isCurry ? undefined : holders, - newPartials = isCurry ? partials : undefined, - newPartialsRight = isCurry ? undefined : partials; - - bitmask |= (isCurry ? PARTIAL_FLAG : PARTIAL_RIGHT_FLAG); - bitmask &= ~(isCurry ? PARTIAL_RIGHT_FLAG : PARTIAL_FLAG); - - if (!(bitmask & CURRY_BOUND_FLAG)) { - bitmask &= ~(BIND_FLAG | BIND_KEY_FLAG); - } - var newData = [func, bitmask, thisArg, newPartials, newsHolders, newPartialsRight, newHoldersRight, newArgPos, ary, arity], - result = wrapFunc.apply(undefined, newData); - - if (isLaziable(func)) { - setData(result, newData); - } - result.placeholder = placeholder; - return result; - } - - /** - * Creates a function like `_.round`. - * - * @private - * @param {string} methodName The name of the `Math` method to use when rounding. - * @returns {Function} Returns the new round function. - */ - function createRound(methodName) { - var func = Math[methodName]; - return function(number, precision) { - number = toNumber(number); - precision = toInteger(precision); - if (precision) { - // Shift with exponential notation to avoid floating-point issues. - // See [MDN](https://mdn.io/round#Examples) for more details. - var pair = (toString(number) + 'e').split('e'), - value = func(pair[0] + 'e' + (+pair[1] + precision)); - - pair = (toString(value) + 'e').split('e'); - return +(pair[0] + 'e' + (+pair[1] - precision)); - } - return func(number); - }; - } - - /** - * Creates a set of `values`. - * - * @private - * @param {Array} values The values to add to the set. - * @returns {Object} Returns the new set. - */ - var createSet = !(Set && new Set([1, 2]).size === 2) ? noop : function(values) { - return new Set(values); - }; - - /** - * Creates a function that either curries or invokes `func` with optional - * `this` binding and partially applied arguments. - * - * @private - * @param {Function|string} func The function or method name to wrap. - * @param {number} bitmask The bitmask of wrapper flags. - * The bitmask may be composed of the following flags: - * 1 - `_.bind` - * 2 - `_.bindKey` - * 4 - `_.curry` or `_.curryRight` of a bound function - * 8 - `_.curry` - * 16 - `_.curryRight` - * 32 - `_.partial` - * 64 - `_.partialRight` - * 128 - `_.rearg` - * 256 - `_.ary` - * @param {*} [thisArg] The `this` binding of `func`. - * @param {Array} [partials] The arguments to be partially applied. - * @param {Array} [holders] The `partials` placeholder indexes. - * @param {Array} [argPos] The argument positions of the new function. - * @param {number} [ary] The arity cap of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createWrapper(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { - var isBindKey = bitmask & BIND_KEY_FLAG; - if (!isBindKey && typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - var length = partials ? partials.length : 0; - if (!length) { - bitmask &= ~(PARTIAL_FLAG | PARTIAL_RIGHT_FLAG); - partials = holders = undefined; - } - ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); - arity = arity === undefined ? arity : toInteger(arity); - length -= holders ? holders.length : 0; - - if (bitmask & PARTIAL_RIGHT_FLAG) { - var partialsRight = partials, - holdersRight = holders; - - partials = holders = undefined; - } - var data = isBindKey ? undefined : getData(func), - newData = [func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity]; - - if (data) { - mergeData(newData, data); - } - func = newData[0]; - bitmask = newData[1]; - thisArg = newData[2]; - partials = newData[3]; - holders = newData[4]; - arity = newData[9] = newData[9] == null - ? (isBindKey ? 0 : func.length) - : nativeMax(newData[9] - length, 0); - - if (!arity && bitmask & (CURRY_FLAG | CURRY_RIGHT_FLAG)) { - bitmask &= ~(CURRY_FLAG | CURRY_RIGHT_FLAG); - } - if (!bitmask || bitmask == BIND_FLAG) { - var result = createBaseWrapper(func, bitmask, thisArg); - } else if (bitmask == CURRY_FLAG || bitmask == CURRY_RIGHT_FLAG) { - result = createCurryWrapper(func, bitmask, arity); - } else if ((bitmask == PARTIAL_FLAG || bitmask == (BIND_FLAG | PARTIAL_FLAG)) && !holders.length) { - result = createPartialWrapper(func, bitmask, thisArg, partials); - } else { - result = createHybridWrapper.apply(undefined, newData); - } - var setter = data ? baseSetData : setData; - return setter(result, newData); - } - - /** - * A specialized version of `baseIsEqualDeep` for arrays with support for - * partial deep comparisons. - * - * @private - * @param {Array} array The array to compare. - * @param {Array} other The other array to compare. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Function} [customizer] The function to customize comparisons. - * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual` for more details. - * @param {Object} [stack] Tracks traversed `array` and `other` objects. - * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. - */ - function equalArrays(array, other, equalFunc, customizer, bitmask, stack) { - var index = -1, - isPartial = bitmask & PARTIAL_COMPARE_FLAG, - isUnordered = bitmask & UNORDERED_COMPARE_FLAG, - arrLength = array.length, - othLength = other.length; - - if (arrLength != othLength && !(isPartial && othLength > arrLength)) { - return false; - } - // Assume cyclic values are equal. - var stacked = stack.get(array); - if (stacked) { - return stacked == other; - } - var result = true; - stack.set(array, other); - - // Ignore non-index properties. - while (++index < arrLength) { - var arrValue = array[index], - othValue = other[index]; - - if (customizer) { - var compared = isPartial - ? customizer(othValue, arrValue, index, other, array, stack) - : customizer(arrValue, othValue, index, array, other, stack); - } - if (compared !== undefined) { - if (compared) { - continue; - } - result = false; - break; - } - // Recursively compare arrays (susceptible to call stack limits). - if (isUnordered) { - if (!arraySome(other, function(othValue) { - return arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack); - })) { - result = false; - break; - } - } else if (!(arrValue === othValue || equalFunc(arrValue, othValue, customizer, bitmask, stack))) { - result = false; - break; - } - } - stack['delete'](array); - return result; - } - - /** - * A specialized version of `baseIsEqualDeep` for comparing objects of - * the same `toStringTag`. - * - * **Note:** This function only supports comparing values with tags of - * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {string} tag The `toStringTag` of the objects to compare. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Function} [customizer] The function to customize comparisons. - * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual` for more details. - * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. - */ - function equalByTag(object, other, tag, equalFunc, customizer, bitmask) { - switch (tag) { - case arrayBufferTag: - if ((object.byteLength != other.byteLength) || - !equalFunc(new Uint8Array(object), new Uint8Array(other))) { - return false; - } - return true; - - case boolTag: - case dateTag: - // Coerce dates and booleans to numbers, dates to milliseconds and booleans - // to `1` or `0` treating invalid dates coerced to `NaN` as not equal. - return +object == +other; - - case errorTag: - return object.name == other.name && object.message == other.message; - - case numberTag: - // Treat `NaN` vs. `NaN` as equal. - return (object != +object) ? other != +other : object == +other; - - case regexpTag: - case stringTag: - // Coerce regexes to strings and treat strings primitives and string - // objects as equal. See https://es5.github.io/#x15.10.6.4 for more details. - return object == (other + ''); - - case mapTag: - var convert = mapToArray; - - case setTag: - var isPartial = bitmask & PARTIAL_COMPARE_FLAG; - convert || (convert = setToArray); - - // Recursively compare objects (susceptible to call stack limits). - return (isPartial || object.size == other.size) && - equalFunc(convert(object), convert(other), customizer, bitmask | UNORDERED_COMPARE_FLAG); - - case symbolTag: - return !!_Symbol && (symbolValueOf.call(object) == symbolValueOf.call(other)); - } - return false; - } - - /** - * A specialized version of `baseIsEqualDeep` for objects with support for - * partial deep comparisons. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Function} [customizer] The function to customize comparisons. - * @param {number} [bitmask] The bitmask of comparison flags. See `baseIsEqual` for more details. - * @param {Object} [stack] Tracks traversed `object` and `other` objects. - * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. - */ - function equalObjects(object, other, equalFunc, customizer, bitmask, stack) { - var isPartial = bitmask & PARTIAL_COMPARE_FLAG, - isUnordered = bitmask & UNORDERED_COMPARE_FLAG, - objProps = keys(object), - objLength = objProps.length, - othProps = keys(other), - othLength = othProps.length; - - if (objLength != othLength && !isPartial) { - return false; - } - var index = objLength; - while (index--) { - var key = objProps[index]; - if (!(isPartial ? key in other : baseHas(other, key)) || - !(isUnordered || key == othProps[index])) { - return false; - } - } - // Assume cyclic values are equal. - var stacked = stack.get(object); - if (stacked) { - return stacked == other; - } - var result = true; - stack.set(object, other); - - var skipCtor = isPartial; - while (++index < objLength) { - key = objProps[index]; - var objValue = object[key], - othValue = other[key]; - - if (customizer) { - var compared = isPartial - ? customizer(othValue, objValue, key, other, object, stack) - : customizer(objValue, othValue, key, object, other, stack); - } - // Recursively compare objects (susceptible to call stack limits). - if (!(compared === undefined - ? (objValue === othValue || equalFunc(objValue, othValue, customizer, bitmask, stack)) - : compared - )) { - result = false; - break; - } - skipCtor || (skipCtor = key == 'constructor'); - } - if (result && !skipCtor) { - var objCtor = object.constructor, - othCtor = other.constructor; - - // Non `Object` object instances with different constructors are not equal. - if (objCtor != othCtor && - ('constructor' in object && 'constructor' in other) && - !(typeof objCtor == 'function' && objCtor instanceof objCtor && - typeof othCtor == 'function' && othCtor instanceof othCtor)) { - result = false; - } - } - stack['delete'](object); - return result; - } - - /** - * Gets metadata for `func`. - * - * @private - * @param {Function} func The function to query. - * @returns {*} Returns the metadata for `func`. - */ - var getData = !metaMap ? noop : function(func) { - return metaMap.get(func); - }; - - /** - * Gets the name of `func`. - * - * @private - * @param {Function} func The function to query. - * @returns {string} Returns the function name. - */ - function getFuncName(func) { - var result = (func.name + ''), - array = realNames[result], - length = array ? array.length : 0; - - while (length--) { - var data = array[length], - otherFunc = data.func; - if (otherFunc == null || otherFunc == func) { - return data.name; - } - } - return result; - } - - /** - * Gets the appropriate "iteratee" function. If the `_.iteratee` method is - * customized this function returns the custom method, otherwise it returns - * `baseIteratee`. If arguments are provided the chosen function is invoked - * with them and its result is returned. - * - * @private - * @param {*} [value] The value to convert to an iteratee. - * @param {number} [arity] The arity of the created iteratee. - * @returns {Function} Returns the chosen function or its result. - */ - function getIteratee() { - var result = lodash.iteratee || iteratee; - result = result === iteratee ? baseIteratee : result; - return arguments.length ? result(arguments[0], arguments[1]) : result; - } - - /** - * Gets the "length" property value of `object`. - * - * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792) - * that affects Safari on at least iOS 8.1-8.3 ARM64. - * - * @private - * @param {Object} object The object to query. - * @returns {*} Returns the "length" value. - */ - var getLength = baseProperty('length'); - - /** - * Gets the property names, values, and compare flags of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the match data of `object`. - */ - function getMatchData(object) { - var result = toPairs(object), - length = result.length; - - while (length--) { - result[length][2] = isStrictComparable(result[length][1]); - } - return result; - } - - /** - * Gets the native function at `key` of `object`. - * - * @private - * @param {Object} object The object to query. - * @param {string} key The key of the method to get. - * @returns {*} Returns the function if it's native, else `undefined`. - */ - function getNative(object, key) { - var value = object == null ? undefined : object[key]; - return isNative(value) ? value : undefined; - } - - /** - * Creates an array of the own symbol properties of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of symbols. - */ - var getSymbols = getOwnPropertySymbols || function() { - return []; - }; - - /** - * Gets the `toStringTag` of `value`. - * - * @private - * @param {*} value The value to query. - * @returns {string} Returns the `toStringTag`. - */ - function getTag(value) { - return objectToString.call(value); - } - - // Fallback for IE 11 providing `toStringTag` values for maps and sets. - if ((Map && getTag(new Map) != mapTag) || (Set && getTag(new Set) != setTag)) { - getTag = function(value) { - var result = objectToString.call(value), - Ctor = result == objectTag ? value.constructor : null, - ctorString = typeof Ctor == 'function' ? funcToString.call(Ctor) : ''; - - if (ctorString) { - if (ctorString == mapCtorString) { - return mapTag; - } - if (ctorString == setCtorString) { - return setTag; - } - } - return result; - }; - } - - /** - * Gets the view, applying any `transforms` to the `start` and `end` positions. - * - * @private - * @param {number} start The start of the view. - * @param {number} end The end of the view. - * @param {Array} transforms The transformations to apply to the view. - * @returns {Object} Returns an object containing the `start` and `end` - * positions of the view. - */ - function getView(start, end, transforms) { - var index = -1, - length = transforms.length; - - while (++index < length) { - var data = transforms[index], - size = data.size; - - switch (data.type) { - case 'drop': start += size; break; - case 'dropRight': end -= size; break; - case 'take': end = nativeMin(end, start + size); break; - case 'takeRight': start = nativeMax(start, end - size); break; - } - } - return { 'start': start, 'end': end }; - } - - /** - * Checks if `path` exists on `object`. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path to check. - * @param {Function} hasFunc The function to check properties. - * @returns {boolean} Returns `true` if `path` exists, else `false`. - */ - function hasPath(object, path, hasFunc) { - if (object == null) { - return false; - } - var result = hasFunc(object, path); - if (!result && !isKey(path)) { - path = baseToPath(path); - object = parent(object, path); - if (object != null) { - path = last(path); - result = hasFunc(object, path); - } - } - return result || (isLength(object && object.length) && isIndex(path, object.length) && - (isArray(object) || isString(object) || isArguments(object))); - } - - /** - * Initializes an array clone. - * - * @private - * @param {Array} array The array to clone. - * @returns {Array} Returns the initialized clone. - */ - function initCloneArray(array) { - var length = array.length, - result = array.constructor(length); - - // Add properties assigned by `RegExp#exec`. - if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { - result.index = array.index; - result.input = array.input; - } - return result; - } - - /** - * Initializes an object clone. - * - * @private - * @param {Object} object The object to clone. - * @returns {Object} Returns the initialized clone. - */ - function initCloneObject(object) { - var Ctor = object.constructor; - return baseCreate(isFunction(Ctor) ? Ctor.prototype : undefined); - } - - /** - * Initializes an object clone based on its `toStringTag`. - * - * **Note:** This function only supports cloning values with tags of - * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. - * - * @private - * @param {Object} object The object to clone. - * @param {string} tag The `toStringTag` of the object to clone. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the initialized clone. - */ - function initCloneByTag(object, tag, isDeep) { - var Ctor = object.constructor; - switch (tag) { - case arrayBufferTag: - return cloneBuffer(object); - - case boolTag: - case dateTag: - return new Ctor(+object); - - case float32Tag: case float64Tag: - case int8Tag: case int16Tag: case int32Tag: - case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: - return cloneTypedArray(object, isDeep); - - case mapTag: - return cloneMap(object); - - case numberTag: - case stringTag: - return new Ctor(object); - - case regexpTag: - return cloneRegExp(object); - - case setTag: - return cloneSet(object); - - case symbolTag: - return cloneSymbol(object); - } - } - - /** - * Creates an array of index keys for `object` values of arrays, - * `arguments` objects, and strings, otherwise `null` is returned. - * - * @private - * @param {Object} object The object to query. - * @returns {Array|null} Returns index keys, else `null`. - */ - function indexKeys(object) { - var length = object ? object.length : undefined; - return (isLength(length) && (isArray(object) || isString(object) || isArguments(object))) - ? baseTimes(length, String) - : null; - } - - /** - * Checks if the provided arguments are from an iteratee call. - * - * @private - * @param {*} value The potential iteratee value argument. - * @param {*} index The potential iteratee index or key argument. - * @param {*} object The potential iteratee object argument. - * @returns {boolean} Returns `true` if the arguments are from an iteratee call, else `false`. - */ - function isIterateeCall(value, index, object) { - if (!isObject(object)) { - return false; - } - var type = typeof index; - if (type == 'number' - ? (isArrayLike(object) && isIndex(index, object.length)) - : (type == 'string' && index in object)) { - return eq(object[index], value); - } - return false; - } - - /** - * Checks if `value` is a property name and not a property path. - * - * @private - * @param {*} value The value to check. - * @param {Object} [object] The object to query keys on. - * @returns {boolean} Returns `true` if `value` is a property name, else `false`. - */ - function isKey(value, object) { - if (typeof value == 'number') { - return true; - } - return !isArray(value) && - (reIsPlainProp.test(value) || !reIsDeepProp.test(value) || - (object != null && value in Object(object))); - } - - /** - * Checks if `value` is suitable for use as unique object key. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is suitable, else `false`. - */ - function isKeyable(value) { - var type = typeof value; - return type == 'number' || type == 'boolean' || - (type == 'string' && value !== '__proto__') || value == null; - } - - /** - * Checks if `func` has a lazy counterpart. - * - * @private - * @param {Function} func The function to check. - * @returns {boolean} Returns `true` if `func` has a lazy counterpart, else `false`. - */ - function isLaziable(func) { - var funcName = getFuncName(func), - other = lodash[funcName]; - - if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) { - return false; - } - if (func === other) { - return true; - } - var data = getData(other); - return !!data && func === data[0]; - } - - /** - * Checks if `value` is likely a prototype object. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. - */ - function isPrototype(value) { - var Ctor = value && value.constructor, - proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; - - return value === proto; - } - - /** - * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` if suitable for strict - * equality comparisons, else `false`. - */ - function isStrictComparable(value) { - return value === value && !isObject(value); - } - - /** - * Merges the function metadata of `source` into `data`. - * - * Merging metadata reduces the number of wrappers used to invoke a function. - * This is possible because methods like `_.bind`, `_.curry`, and `_.partial` - * may be applied regardless of execution order. Methods like `_.ary` and `_.rearg` - * modify function arguments, making the order in which they are executed important, - * preventing the merging of metadata. However, we make an exception for a safe - * combined case where curried functions have `_.ary` and or `_.rearg` applied. - * - * @private - * @param {Array} data The destination metadata. - * @param {Array} source The source metadata. - * @returns {Array} Returns `data`. - */ - function mergeData(data, source) { - var bitmask = data[1], - srcBitmask = source[1], - newBitmask = bitmask | srcBitmask, - isCommon = newBitmask < (BIND_FLAG | BIND_KEY_FLAG | ARY_FLAG); - - var isCombo = - (srcBitmask == ARY_FLAG && (bitmask == CURRY_FLAG)) || - (srcBitmask == ARY_FLAG && (bitmask == REARG_FLAG) && (data[7].length <= source[8])) || - (srcBitmask == (ARY_FLAG | REARG_FLAG) && (source[7].length <= source[8]) && (bitmask == CURRY_FLAG)); - - // Exit early if metadata can't be merged. - if (!(isCommon || isCombo)) { - return data; - } - // Use source `thisArg` if available. - if (srcBitmask & BIND_FLAG) { - data[2] = source[2]; - // Set when currying a bound function. - newBitmask |= (bitmask & BIND_FLAG) ? 0 : CURRY_BOUND_FLAG; - } - // Compose partial arguments. - var value = source[3]; - if (value) { - var partials = data[3]; - data[3] = partials ? composeArgs(partials, value, source[4]) : copyArray(value); - data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : copyArray(source[4]); - } - // Compose partial right arguments. - value = source[5]; - if (value) { - partials = data[5]; - data[5] = partials ? composeArgsRight(partials, value, source[6]) : copyArray(value); - data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : copyArray(source[6]); - } - // Use source `argPos` if available. - value = source[7]; - if (value) { - data[7] = copyArray(value); - } - // Use source `ary` if it's smaller. - if (srcBitmask & ARY_FLAG) { - data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]); - } - // Use source `arity` if one is not provided. - if (data[9] == null) { - data[9] = source[9]; - } - // Use source `func` and merge bitmasks. - data[0] = source[0]; - data[1] = newBitmask; - - return data; - } - - /** - * Used by `_.defaultsDeep` to customize its `_.merge` use. - * - * @private - * @param {*} objValue The destination value. - * @param {*} srcValue The source value. - * @param {string} key The key of the property to merge. - * @param {Object} object The parent object of `objValue`. - * @param {Object} source The parent object of `srcValue`. - * @param {Object} [stack] Tracks traversed source values and their merged counterparts. - * @returns {*} Returns the value to assign. - */ - function mergeDefaults(objValue, srcValue, key, object, source, stack) { - if (isObject(objValue) && isObject(srcValue)) { - stack.set(srcValue, objValue); - baseMerge(objValue, srcValue, mergeDefaults, stack); - } - return objValue === undefined ? baseClone(srcValue) : objValue; - } - - /** - * Gets the parent value at `path` of `object`. - * - * @private - * @param {Object} object The object to query. - * @param {Array} path The path to get the parent value of. - * @returns {*} Returns the parent value. - */ - function parent(object, path) { - return path.length == 1 ? object : get(object, baseSlice(path, 0, -1)); - } - - /** - * Reorder `array` according to the specified indexes where the element at - * the first index is assigned as the first element, the element at - * the second index is assigned as the second element, and so on. - * - * @private - * @param {Array} array The array to reorder. - * @param {Array} indexes The arranged array indexes. - * @returns {Array} Returns `array`. - */ - function reorder(array, indexes) { - var arrLength = array.length, - length = nativeMin(indexes.length, arrLength), - oldArray = copyArray(array); - - while (length--) { - var index = indexes[length]; - array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined; - } - return array; - } - - /** - * Sets metadata for `func`. - * - * **Note:** If this function becomes hot, i.e. is invoked a lot in a short - * period of time, it will trip its breaker and transition to an identity function - * to avoid garbage collection pauses in V8. See [V8 issue 2070](https://code.google.com/p/v8/issues/detail?id=2070) - * for more details. - * - * @private - * @param {Function} func The function to associate metadata with. - * @param {*} data The metadata. - * @returns {Function} Returns `func`. - */ - var setData = (function() { - var count = 0, - lastCalled = 0; - - return function(key, value) { - var stamp = now(), - remaining = HOT_SPAN - (stamp - lastCalled); - - lastCalled = stamp; - if (remaining > 0) { - if (++count >= HOT_COUNT) { - return key; - } - } else { - count = 0; - } - return baseSetData(key, value); - }; - }()); - - /** - * Converts `string` to a property path array. - * - * @private - * @param {string} string The string to convert. - * @returns {Array} Returns the property path array. - */ - function stringToPath(string) { - var result = []; - toString(string).replace(rePropName, function(match, number, quote, string) { - result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match)); - }); - return result; - } - - /** - * Converts `value` to an array-like object if it's not one. - * - * @private - * @param {*} value The value to process. - * @returns {Array} Returns the array-like object. - */ - function toArrayLikeObject(value) { - return isArrayLikeObject(value) ? value : []; - } - - /** - * Converts `value` to a function if it's not one. - * - * @private - * @param {*} value The value to process. - * @returns {Function} Returns the function. - */ - function toFunction(value) { - return typeof value == 'function' ? value : identity; - } - - /** - * Creates a clone of `wrapper`. - * - * @private - * @param {Object} wrapper The wrapper to clone. - * @returns {Object} Returns the cloned wrapper. - */ - function wrapperClone(wrapper) { - if (wrapper instanceof LazyWrapper) { - return wrapper.clone(); - } - var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__); - result.__actions__ = copyArray(wrapper.__actions__); - result.__index__ = wrapper.__index__; - result.__values__ = wrapper.__values__; - return result; - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates an array of elements split into groups the length of `size`. - * If `array` can't be split evenly, the final chunk will be the remaining - * elements. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to process. - * @param {number} [size=0] The length of each chunk. - * @returns {Array} Returns the new array containing chunks. - * @example - * - * _.chunk(['a', 'b', 'c', 'd'], 2); - * // => [['a', 'b'], ['c', 'd']] - * - * _.chunk(['a', 'b', 'c', 'd'], 3); - * // => [['a', 'b', 'c'], ['d']] - */ - function chunk(array, size) { - size = nativeMax(toInteger(size), 0); - - var length = array ? array.length : 0; - if (!length || size < 1) { - return []; - } - var index = 0, - resIndex = -1, - result = Array(nativeCeil(length / size)); - - while (index < length) { - result[++resIndex] = baseSlice(array, index, (index += size)); - } - return result; - } - - /** - * Creates an array with all falsey values removed. The values `false`, `null`, - * `0`, `""`, `undefined`, and `NaN` are falsey. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to compact. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.compact([0, 1, false, 2, '', 3]); - * // => [1, 2, 3] - */ - function compact(array) { - var index = -1, - length = array ? array.length : 0, - resIndex = -1, - result = []; - - while (++index < length) { - var value = array[index]; - if (value) { - result[++resIndex] = value; - } - } - return result; - } - - /** - * Creates a new array concatenating `array` with any additional arrays - * and/or values. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to concatenate. - * @param {...*} [values] The values to concatenate. - * @returns {Array} Returns the new concatenated array. - * @example - * - * var array = [1]; - * var other = _.concat(array, 2, [3], [[4]]); - * - * console.log(other); - * // => [1, 2, 3, [4]] - * - * console.log(array); - * // => [1] - */ - var concat = rest(function(array, values) { - values = baseFlatten(values); - return arrayConcat(isArray(array) ? array : [Object(array)], values); - }); - - /** - * Creates an array of unique `array` values not included in the other - * provided arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @param {...Array} [values] The values to exclude. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.difference([3, 2, 1], [4, 2]); - * // => [3, 1] - */ - var difference = rest(function(array, values) { - return isArrayLikeObject(array) - ? baseDifference(array, baseFlatten(values, false, true)) - : []; - }); - - /** - * This method is like `_.difference` except that it accepts `iteratee` which - * is invoked for each element of `array` and `values` to generate the criterion - * by which uniqueness is computed. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @param {...Array} [values] The values to exclude. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor); - * // => [3.1, 1.3] - * - * // using the `_.property` iteratee shorthand - * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x'); - * // => [{ 'x': 2 }] - */ - var differenceBy = rest(function(array, values) { - var iteratee = last(values); - if (isArrayLikeObject(iteratee)) { - iteratee = undefined; - } - return isArrayLikeObject(array) - ? baseDifference(array, baseFlatten(values, false, true), getIteratee(iteratee)) - : []; - }); - - /** - * This method is like `_.difference` except that it accepts `comparator` - * which is invoked to compare elements of `array` to `values`. The comparator - * is invoked with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @param {...Array} [values] The values to exclude. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * - * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual); - * // => [{ 'x': 2, 'y': 1 }] - */ - var differenceWith = rest(function(array, values) { - var comparator = last(values); - if (isArrayLikeObject(comparator)) { - comparator = undefined; - } - return isArrayLikeObject(array) - ? baseDifference(array, baseFlatten(values, false, true), undefined, comparator) - : []; - }); - - /** - * Creates a slice of `array` with `n` elements dropped from the beginning. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to drop. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.drop([1, 2, 3]); - * // => [2, 3] - * - * _.drop([1, 2, 3], 2); - * // => [3] - * - * _.drop([1, 2, 3], 5); - * // => [] - * - * _.drop([1, 2, 3], 0); - * // => [1, 2, 3] - */ - function drop(array, n, guard) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - return baseSlice(array, n < 0 ? 0 : n, length); - } - - /** - * Creates a slice of `array` with `n` elements dropped from the end. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to drop. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.dropRight([1, 2, 3]); - * // => [1, 2] - * - * _.dropRight([1, 2, 3], 2); - * // => [1] - * - * _.dropRight([1, 2, 3], 5); - * // => [] - * - * _.dropRight([1, 2, 3], 0); - * // => [1, 2, 3] - */ - function dropRight(array, n, guard) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - n = length - n; - return baseSlice(array, 0, n < 0 ? 0 : n); - } - - /** - * Creates a slice of `array` excluding elements dropped from the end. - * Elements are dropped until `predicate` returns falsey. The predicate is - * invoked with three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': false } - * ]; - * - * _.dropRightWhile(users, function(o) { return !o.active; }); - * // => objects for ['barney'] - * - * // using the `_.matches` iteratee shorthand - * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false }); - * // => objects for ['barney', 'fred'] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.dropRightWhile(users, ['active', false]); - * // => objects for ['barney'] - * - * // using the `_.property` iteratee shorthand - * _.dropRightWhile(users, 'active'); - * // => objects for ['barney', 'fred', 'pebbles'] - */ - function dropRightWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3), true, true) - : []; - } - - /** - * Creates a slice of `array` excluding elements dropped from the beginning. - * Elements are dropped until `predicate` returns falsey. The predicate is - * invoked with three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': true } - * ]; - * - * _.dropWhile(users, function(o) { return !o.active; }); - * // => objects for ['pebbles'] - * - * // using the `_.matches` iteratee shorthand - * _.dropWhile(users, { 'user': 'barney', 'active': false }); - * // => objects for ['fred', 'pebbles'] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.dropWhile(users, ['active', false]); - * // => objects for ['pebbles'] - * - * // using the `_.property` iteratee shorthand - * _.dropWhile(users, 'active'); - * // => objects for ['barney', 'fred', 'pebbles'] - */ - function dropWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3), true) - : []; - } - - /** - * Fills elements of `array` with `value` from `start` up to, but not - * including, `end`. - * - * **Note:** This method mutates `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to fill. - * @param {*} value The value to fill `array` with. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3]; - * - * _.fill(array, 'a'); - * console.log(array); - * // => ['a', 'a', 'a'] - * - * _.fill(Array(3), 2); - * // => [2, 2, 2] - * - * _.fill([4, 6, 8, 10], '*', 1, 3); - * // => [4, '*', '*', 10] - */ - function fill(array, value, start, end) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { - start = 0; - end = length; - } - return baseFill(array, value, start, end); - } - - /** - * This method is like `_.find` except that it returns the index of the first - * element `predicate` returns truthy for instead of the element itself. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to search. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {number} Returns the index of the found element, else `-1`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': true } - * ]; - * - * _.findIndex(users, function(o) { return o.user == 'barney'; }); - * // => 0 - * - * // using the `_.matches` iteratee shorthand - * _.findIndex(users, { 'user': 'fred', 'active': false }); - * // => 1 - * - * // using the `_.matchesProperty` iteratee shorthand - * _.findIndex(users, ['active', false]); - * // => 0 - * - * // using the `_.property` iteratee shorthand - * _.findIndex(users, 'active'); - * // => 2 - */ - function findIndex(array, predicate) { - return (array && array.length) - ? baseFindIndex(array, getIteratee(predicate, 3)) - : -1; - } - - /** - * This method is like `_.findIndex` except that it iterates over elements - * of `collection` from right to left. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to search. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {number} Returns the index of the found element, else `-1`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': false } - * ]; - * - * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; }); - * // => 2 - * - * // using the `_.matches` iteratee shorthand - * _.findLastIndex(users, { 'user': 'barney', 'active': true }); - * // => 0 - * - * // using the `_.matchesProperty` iteratee shorthand - * _.findLastIndex(users, ['active', false]); - * // => 2 - * - * // using the `_.property` iteratee shorthand - * _.findLastIndex(users, 'active'); - * // => 0 - */ - function findLastIndex(array, predicate) { - return (array && array.length) - ? baseFindIndex(array, getIteratee(predicate, 3), true) - : -1; - } - - /** - * Creates an array of flattened values by running each element in `array` - * through `iteratee` and concating its result to the other mapped values. - * The iteratee is invoked with three arguments: (value, index|key, array). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new array. - * @example - * - * function duplicate(n) { - * return [n, n]; - * } - * - * _.flatMap([1, 2], duplicate); - * // => [1, 1, 2, 2] - */ - function flatMap(array, iteratee) { - var length = array ? array.length : 0; - return length ? baseFlatten(arrayMap(array, getIteratee(iteratee, 3))) : []; - } - - /** - * Flattens `array` a single level. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to flatten. - * @returns {Array} Returns the new flattened array. - * @example - * - * _.flatten([1, [2, 3, [4]]]); - * // => [1, 2, 3, [4]] - */ - function flatten(array) { - var length = array ? array.length : 0; - return length ? baseFlatten(array) : []; - } - - /** - * This method is like `_.flatten` except that it recursively flattens `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to recursively flatten. - * @returns {Array} Returns the new flattened array. - * @example - * - * _.flattenDeep([1, [2, 3, [4]]]); - * // => [1, 2, 3, 4] - */ - function flattenDeep(array) { - var length = array ? array.length : 0; - return length ? baseFlatten(array, true) : []; - } - - /** - * The inverse of `_.toPairs`; this method returns an object composed - * from key-value `pairs`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} pairs The key-value pairs. - * @returns {Object} Returns the new object. - * @example - * - * _.fromPairs([['fred', 30], ['barney', 40]]); - * // => { 'fred': 30, 'barney': 40 } - */ - function fromPairs(pairs) { - var index = -1, - length = pairs ? pairs.length : 0, - result = {}; - - while (++index < length) { - var pair = pairs[index]; - baseSet(result, pair[0], pair[1]); - } - return result; - } - - /** - * Gets the first element of `array`. - * - * @static - * @memberOf _ - * @alias first - * @category Array - * @param {Array} array The array to query. - * @returns {*} Returns the first element of `array`. - * @example - * - * _.head([1, 2, 3]); - * // => 1 - * - * _.head([]); - * // => undefined - */ - function head(array) { - return array ? array[0] : undefined; - } - - /** - * Gets the index at which the first occurrence of `value` is found in `array` - * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. If `fromIndex` is negative, it's used as the offset - * from the end of `array`. If `array` is sorted providing `true` for `fromIndex` - * performs a faster binary search. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @param {number} [fromIndex=0] The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.indexOf([1, 2, 1, 2], 2); - * // => 1 - * - * // using `fromIndex` - * _.indexOf([1, 2, 1, 2], 2, 2); - * // => 3 - */ - function indexOf(array, value, fromIndex) { - var length = array ? array.length : 0; - if (!length) { - return -1; - } - fromIndex = toInteger(fromIndex); - if (fromIndex < 0) { - fromIndex = nativeMax(length + fromIndex, 0); - } - return baseIndexOf(array, value, fromIndex); - } - - /** - * Gets all but the last element of `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.initial([1, 2, 3]); - * // => [1, 2] - */ - function initial(array) { - return dropRight(array, 1); - } - - /** - * Creates an array of unique values that are included in all of the provided - * arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @returns {Array} Returns the new array of shared values. - * @example - * _.intersection([2, 1], [4, 2], [1, 2]); - * // => [2] - */ - var intersection = rest(function(arrays) { - var mapped = arrayMap(arrays, toArrayLikeObject); - return (mapped.length && mapped[0] === arrays[0]) - ? baseIntersection(mapped) - : []; - }); - - /** - * This method is like `_.intersection` except that it accepts `iteratee` - * which is invoked for each element of each `arrays` to generate the criterion - * by which uniqueness is computed. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of shared values. - * @example - * - * _.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor); - * // => [2.1] - * - * // using the `_.property` iteratee shorthand - * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }] - */ - var intersectionBy = rest(function(arrays) { - var iteratee = last(arrays), - mapped = arrayMap(arrays, toArrayLikeObject); - - if (iteratee === last(mapped)) { - iteratee = undefined; - } else { - mapped.pop(); - } - return (mapped.length && mapped[0] === arrays[0]) - ? baseIntersection(mapped, getIteratee(iteratee)) - : []; - }); - - /** - * This method is like `_.intersection` except that it accepts `comparator` - * which is invoked to compare elements of `arrays`. The comparator is invoked - * with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of shared values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.intersectionWith(objects, others, _.isEqual); - * // => [{ 'x': 1, 'y': 2 }] - */ - var intersectionWith = rest(function(arrays) { - var comparator = last(arrays), - mapped = arrayMap(arrays, toArrayLikeObject); - - if (comparator === last(mapped)) { - comparator = undefined; - } else { - mapped.pop(); - } - return (mapped.length && mapped[0] === arrays[0]) - ? baseIntersection(mapped, undefined, comparator) - : []; - }); - - /** - * Converts all elements in `array` into a string separated by `separator`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to convert. - * @param {string} [separator=','] The element separator. - * @returns {string} Returns the joined string. - * @example - * - * _.join(['a', 'b', 'c'], '~'); - * // => 'a~b~c' - */ - function join(array, separator) { - return array ? nativeJoin.call(array, separator) : ''; - } - - /** - * Gets the last element of `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @returns {*} Returns the last element of `array`. - * @example - * - * _.last([1, 2, 3]); - * // => 3 - */ - function last(array) { - var length = array ? array.length : 0; - return length ? array[length - 1] : undefined; - } - - /** - * This method is like `_.indexOf` except that it iterates over elements of - * `array` from right to left. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @param {number} [fromIndex=array.length-1] The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.lastIndexOf([1, 2, 1, 2], 2); - * // => 3 - * - * // using `fromIndex` - * _.lastIndexOf([1, 2, 1, 2], 2, 2); - * // => 1 - */ - function lastIndexOf(array, value, fromIndex) { - var length = array ? array.length : 0; - if (!length) { - return -1; - } - var index = length; - if (fromIndex !== undefined) { - index = toInteger(fromIndex); - index = (index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1)) + 1; - } - if (value !== value) { - return indexOfNaN(array, index, true); - } - while (index--) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * Removes all provided values from `array` using - * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. - * - * **Note:** Unlike `_.without`, this method mutates `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to modify. - * @param {...*} [values] The values to remove. - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3, 1, 2, 3]; - * - * _.pull(array, 2, 3); - * console.log(array); - * // => [1, 1] - */ - var pull = rest(pullAll); - - /** - * This method is like `_.pull` except that it accepts an array of values to remove. - * - * **Note:** Unlike `_.difference`, this method mutates `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3, 1, 2, 3]; - * - * _.pull(array, [2, 3]); - * console.log(array); - * // => [1, 1] - */ - function pullAll(array, values) { - return (array && array.length && values && values.length) - ? basePullAll(array, values) - : array; - } - - /** - * This method is like `_.pullAll` except that it accepts `iteratee` which is - * invoked for each element of `array` and `values` to to generate the criterion - * by which uniqueness is computed. The iteratee is invoked with one argument: (value). - * - * **Note:** Unlike `_.differenceBy`, this method mutates `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns `array`. - * @example - * - * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]; - * - * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x'); - * console.log(array); - * // => [{ 'x': 2 }] - */ - function pullAllBy(array, values, iteratee) { - return (array && array.length && values && values.length) - ? basePullAllBy(array, values, getIteratee(iteratee)) - : array; - } - - /** - * Removes elements from `array` corresponding to `indexes` and returns an - * array of removed elements. - * - * **Note:** Unlike `_.at`, this method mutates `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to modify. - * @param {...(number|number[])} [indexes] The indexes of elements to remove, - * specified individually or in arrays. - * @returns {Array} Returns the new array of removed elements. - * @example - * - * var array = [5, 10, 15, 20]; - * var evens = _.pullAt(array, 1, 3); - * - * console.log(array); - * // => [5, 15] - * - * console.log(evens); - * // => [10, 20] - */ - var pullAt = rest(function(array, indexes) { - indexes = arrayMap(baseFlatten(indexes), String); - - var result = baseAt(array, indexes); - basePullAt(array, indexes.sort(compareAscending)); - return result; - }); - - /** - * Removes all elements from `array` that `predicate` returns truthy for - * and returns an array of the removed elements. The predicate is invoked with - * three arguments: (value, index, array). - * - * **Note:** Unlike `_.filter`, this method mutates `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to modify. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new array of removed elements. - * @example - * - * var array = [1, 2, 3, 4]; - * var evens = _.remove(array, function(n) { - * return n % 2 == 0; - * }); - * - * console.log(array); - * // => [1, 3] - * - * console.log(evens); - * // => [2, 4] - */ - function remove(array, predicate) { - var result = []; - if (!(array && array.length)) { - return result; - } - var index = -1, - indexes = [], - length = array.length; - - predicate = getIteratee(predicate, 3); - while (++index < length) { - var value = array[index]; - if (predicate(value, index, array)) { - result.push(value); - indexes.push(index); - } - } - basePullAt(array, indexes); - return result; - } - - /** - * Reverses `array` so that the first element becomes the last, the second - * element becomes the second to last, and so on. - * - * **Note:** This method mutates `array` and is based on - * [`Array#reverse`](https://mdn.io/Array/reverse). - * - * @memberOf _ - * @category Array - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3]; - * - * _.reverse(array); - * // => [3, 2, 1] - * - * console.log(array); - * // => [3, 2, 1] - */ - function reverse(array) { - return array ? nativeReverse.call(array) : array; - } - - /** - * Creates a slice of `array` from `start` up to, but not including, `end`. - * - * **Note:** This method is used instead of [`Array#slice`](https://mdn.io/Array/slice) - * to ensure dense arrays are returned. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to slice. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns the slice of `array`. - */ - function slice(array, start, end) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { - start = 0; - end = length; - } - else { - start = start == null ? 0 : toInteger(start); - end = end === undefined ? length : toInteger(end); - } - return baseSlice(array, start, end); - } - - /** - * Uses a binary search to determine the lowest index at which `value` should - * be inserted into `array` in order to maintain its sort order. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @returns {number} Returns the index at which `value` should be inserted into `array`. - * @example - * - * _.sortedIndex([30, 50], 40); - * // => 1 - * - * _.sortedIndex([4, 5], 4); - * // => 0 - */ - function sortedIndex(array, value) { - return baseSortedIndex(array, value); - } - - /** - * This method is like `_.sortedIndex` except that it accepts `iteratee` - * which is invoked for `value` and each element of `array` to compute their - * sort ranking. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {number} Returns the index at which `value` should be inserted into `array`. - * @example - * - * var dict = { 'thirty': 30, 'forty': 40, 'fifty': 50 }; - * - * _.sortedIndexBy(['thirty', 'fifty'], 'forty', _.propertyOf(dict)); - * // => 1 - * - * // using the `_.property` iteratee shorthand - * _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x'); - * // => 0 - */ - function sortedIndexBy(array, value, iteratee) { - return baseSortedIndexBy(array, value, getIteratee(iteratee)); - } - - /** - * This method is like `_.indexOf` except that it performs a binary - * search on a sorted `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.sortedIndexOf([1, 1, 2, 2], 2); - * // => 2 - */ - function sortedIndexOf(array, value) { - var length = array ? array.length : 0; - if (length) { - var index = baseSortedIndex(array, value); - if (index < length && eq(array[index], value)) { - return index; - } - } - return -1; - } - - /** - * This method is like `_.sortedIndex` except that it returns the highest - * index at which `value` should be inserted into `array` in order to - * maintain its sort order. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @returns {number} Returns the index at which `value` should be inserted into `array`. - * @example - * - * _.sortedLastIndex([4, 5], 4); - * // => 1 - */ - function sortedLastIndex(array, value) { - return baseSortedIndex(array, value, true); - } - - /** - * This method is like `_.sortedLastIndex` except that it accepts `iteratee` - * which is invoked for `value` and each element of `array` to compute their - * sort ranking. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {number} Returns the index at which `value` should be inserted into `array`. - * @example - * - * // using the `_.property` iteratee shorthand - * _.sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x'); - * // => 1 - */ - function sortedLastIndexBy(array, value, iteratee) { - return baseSortedIndexBy(array, value, getIteratee(iteratee), true); - } - - /** - * This method is like `_.lastIndexOf` except that it performs a binary - * search on a sorted `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to search. - * @param {*} value The value to search for. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.sortedLastIndexOf([1, 1, 2, 2], 2); - * // => 3 - */ - function sortedLastIndexOf(array, value) { - var length = array ? array.length : 0; - if (length) { - var index = baseSortedIndex(array, value, true) - 1; - if (eq(array[index], value)) { - return index; - } - } - return -1; - } - - /** - * This method is like `_.uniq` except that it's designed and optimized - * for sorted arrays. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.sortedUniq([1, 1, 2]); - * // => [1, 2] - */ - function sortedUniq(array) { - return (array && array.length) - ? baseSortedUniq(array) - : []; - } - - /** - * This method is like `_.uniqBy` except that it's designed and optimized - * for sorted arrays. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor); - * // => [1.1, 2.2] - */ - function sortedUniqBy(array, iteratee) { - return (array && array.length) - ? baseSortedUniqBy(array, getIteratee(iteratee)) - : []; - } - - /** - * Gets all but the first element of `array`. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.tail([1, 2, 3]); - * // => [2, 3] - */ - function tail(array) { - return drop(array, 1); - } - - /** - * Creates a slice of `array` with `n` elements taken from the beginning. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to take. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.take([1, 2, 3]); - * // => [1] - * - * _.take([1, 2, 3], 2); - * // => [1, 2] - * - * _.take([1, 2, 3], 5); - * // => [1, 2, 3] - * - * _.take([1, 2, 3], 0); - * // => [] - */ - function take(array, n, guard) { - if (!(array && array.length)) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - return baseSlice(array, 0, n < 0 ? 0 : n); - } - - /** - * Creates a slice of `array` with `n` elements taken from the end. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to take. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.takeRight([1, 2, 3]); - * // => [3] - * - * _.takeRight([1, 2, 3], 2); - * // => [2, 3] - * - * _.takeRight([1, 2, 3], 5); - * // => [1, 2, 3] - * - * _.takeRight([1, 2, 3], 0); - * // => [] - */ - function takeRight(array, n, guard) { - var length = array ? array.length : 0; - if (!length) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - n = length - n; - return baseSlice(array, n < 0 ? 0 : n, length); - } - - /** - * Creates a slice of `array` with elements taken from the end. Elements are - * taken until `predicate` returns falsey. The predicate is invoked with three - * arguments: (value, index, array). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': false } - * ]; - * - * _.takeRightWhile(users, function(o) { return !o.active; }); - * // => objects for ['fred', 'pebbles'] - * - * // using the `_.matches` iteratee shorthand - * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false }); - * // => objects for ['pebbles'] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.takeRightWhile(users, ['active', false]); - * // => objects for ['fred', 'pebbles'] - * - * // using the `_.property` iteratee shorthand - * _.takeRightWhile(users, 'active'); - * // => [] - */ - function takeRightWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3), false, true) - : []; - } - - /** - * Creates a slice of `array` with elements taken from the beginning. Elements - * are taken until `predicate` returns falsey. The predicate is invoked with - * three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to query. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false}, - * { 'user': 'pebbles', 'active': true } - * ]; - * - * _.takeWhile(users, function(o) { return !o.active; }); - * // => objects for ['barney', 'fred'] - * - * // using the `_.matches` iteratee shorthand - * _.takeWhile(users, { 'user': 'barney', 'active': false }); - * // => objects for ['barney'] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.takeWhile(users, ['active', false]); - * // => objects for ['barney', 'fred'] - * - * // using the `_.property` iteratee shorthand - * _.takeWhile(users, 'active'); - * // => [] - */ - function takeWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3)) - : []; - } - - /** - * Creates an array of unique values, in order, from all of the provided arrays - * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @returns {Array} Returns the new array of combined values. - * @example - * - * _.union([2, 1], [4, 2], [1, 2]); - * // => [2, 1, 4] - */ - var union = rest(function(arrays) { - return baseUniq(baseFlatten(arrays, false, true)); - }); - - /** - * This method is like `_.union` except that it accepts `iteratee` which is - * invoked for each element of each `arrays` to generate the criterion by which - * uniqueness is computed. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of combined values. - * @example - * - * _.unionBy([2.1, 1.2], [4.3, 2.4], Math.floor); - * // => [2.1, 1.2, 4.3] - * - * // using the `_.property` iteratee shorthand - * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - var unionBy = rest(function(arrays) { - var iteratee = last(arrays); - if (isArrayLikeObject(iteratee)) { - iteratee = undefined; - } - return baseUniq(baseFlatten(arrays, false, true), getIteratee(iteratee)); - }); - - /** - * This method is like `_.union` except that it accepts `comparator` which - * is invoked to compare elements of `arrays`. The comparator is invoked - * with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of combined values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.unionWith(objects, others, _.isEqual); - * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] - */ - var unionWith = rest(function(arrays) { - var comparator = last(arrays); - if (isArrayLikeObject(comparator)) { - comparator = undefined; - } - return baseUniq(baseFlatten(arrays, false, true), undefined, comparator); - }); - - /** - * Creates a duplicate-free version of an array, using - * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons, in which only the first occurrence of each element - * is kept. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.uniq([2, 1, 2]); - * // => [2, 1] - */ - function uniq(array) { - return (array && array.length) - ? baseUniq(array) - : []; - } - - /** - * This method is like `_.uniq` except that it accepts `iteratee` which is - * invoked for each element in `array` to generate the criterion by which - * uniqueness is computed. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.uniqBy([2.1, 1.2, 2.3], Math.floor); - * // => [2.1, 1.2] - * - * // using the `_.property` iteratee shorthand - * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - function uniqBy(array, iteratee) { - return (array && array.length) - ? baseUniq(array, getIteratee(iteratee)) - : []; - } - - /** - * This method is like `_.uniq` except that it accepts `comparator` which - * is invoked to compare elements of `array`. The comparator is invoked with - * two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.uniqWith(objects, _.isEqual); - * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }] - */ - function uniqWith(array, comparator) { - return (array && array.length) - ? baseUniq(array, undefined, comparator) - : []; - } - - /** - * This method is like `_.zip` except that it accepts an array of grouped - * elements and creates an array regrouping the elements to their pre-zip - * configuration. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array of grouped elements to process. - * @returns {Array} Returns the new array of regrouped elements. - * @example - * - * var zipped = _.zip(['fred', 'barney'], [30, 40], [true, false]); - * // => [['fred', 30, true], ['barney', 40, false]] - * - * _.unzip(zipped); - * // => [['fred', 'barney'], [30, 40], [true, false]] - */ - function unzip(array) { - if (!(array && array.length)) { - return []; - } - var length = 0; - array = arrayFilter(array, function(group) { - if (isArrayLikeObject(group)) { - length = nativeMax(group.length, length); - return true; - } - }); - return baseTimes(length, function(index) { - return arrayMap(array, baseProperty(index)); - }); - } - - /** - * This method is like `_.unzip` except that it accepts `iteratee` to specify - * how regrouped values should be combined. The iteratee is invoked with the - * elements of each group: (...group). - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array of grouped elements to process. - * @param {Function} [iteratee=_.identity] The function to combine regrouped values. - * @returns {Array} Returns the new array of regrouped elements. - * @example - * - * var zipped = _.zip([1, 2], [10, 20], [100, 200]); - * // => [[1, 10, 100], [2, 20, 200]] - * - * _.unzipWith(zipped, _.add); - * // => [3, 30, 300] - */ - function unzipWith(array, iteratee) { - if (!(array && array.length)) { - return []; - } - var result = unzip(array); - if (iteratee == null) { - return result; - } - return arrayMap(result, function(group) { - return apply(iteratee, undefined, group); - }); - } - - /** - * Creates an array excluding all provided values using - * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * for equality comparisons. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} array The array to filter. - * @param {...*} [values] The values to exclude. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.without([1, 2, 1, 3], 1, 2); - * // => [3] - */ - var without = rest(function(array, values) { - return isArrayLikeObject(array) - ? baseDifference(array, values) - : []; - }); - - /** - * Creates an array of unique values that is the [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) - * of the provided arrays. - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @returns {Array} Returns the new array of values. - * @example - * - * _.xor([2, 1], [4, 2]); - * // => [1, 4] - */ - var xor = rest(function(arrays) { - return baseXor(arrayFilter(arrays, isArrayLikeObject)); - }); - - /** - * This method is like `_.xor` except that it accepts `iteratee` which is - * invoked for each element of each `arrays` to generate the criterion by which - * uniqueness is computed. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of values. - * @example - * - * _.xorBy([2.1, 1.2], [4.3, 2.4], Math.floor); - * // => [1.2, 4.3] - * - * // using the `_.property` iteratee shorthand - * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 2 }] - */ - var xorBy = rest(function(arrays) { - var iteratee = last(arrays); - if (isArrayLikeObject(iteratee)) { - iteratee = undefined; - } - return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee)); - }); - - /** - * This method is like `_.xor` except that it accepts `comparator` which is - * invoked to compare elements of `arrays`. The comparator is invoked with - * two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.xorWith(objects, others, _.isEqual); - * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] - */ - var xorWith = rest(function(arrays) { - var comparator = last(arrays); - if (isArrayLikeObject(comparator)) { - comparator = undefined; - } - return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator); - }); - - /** - * Creates an array of grouped elements, the first of which contains the first - * elements of the given arrays, the second of which contains the second elements - * of the given arrays, and so on. - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to process. - * @returns {Array} Returns the new array of grouped elements. - * @example - * - * _.zip(['fred', 'barney'], [30, 40], [true, false]); - * // => [['fred', 30, true], ['barney', 40, false]] - */ - var zip = rest(unzip); - - /** - * This method is like `_.fromPairs` except that it accepts two arrays, - * one of property names and one of corresponding values. - * - * @static - * @memberOf _ - * @category Array - * @param {Array} [props=[]] The property names. - * @param {Array} [values=[]] The property values. - * @returns {Object} Returns the new object. - * @example - * - * _.zipObject(['fred', 'barney'], [30, 40]); - * // => { 'fred': 30, 'barney': 40 } - */ - function zipObject(props, values) { - var index = -1, - length = props ? props.length : 0, - valsLength = values ? values.length : 0, - result = {}; - - while (++index < length) { - baseSet(result, props[index], index < valsLength ? values[index] : undefined); - } - return result; - } - - /** - * This method is like `_.zip` except that it accepts `iteratee` to specify - * how grouped values should be combined. The iteratee is invoked with the - * elements of each group: (...group). - * - * @static - * @memberOf _ - * @category Array - * @param {...Array} [arrays] The arrays to process. - * @param {Function} [iteratee=_.identity] The function to combine grouped values. - * @returns {Array} Returns the new array of grouped elements. - * @example - * - * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) { - * return a + b + c; - * }); - * // => [111, 222] - */ - var zipWith = rest(function(arrays) { - var length = arrays.length, - iteratee = length > 1 ? arrays[length - 1] : undefined; - - iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined; - return unzipWith(arrays, iteratee); - }); - - /*------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object that wraps `value` with explicit method chaining enabled. - * The result of such method chaining must be unwrapped with `_#value`. - * - * @static - * @memberOf _ - * @category Seq - * @param {*} value The value to wrap. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 40 }, - * { 'user': 'pebbles', 'age': 1 } - * ]; - * - * var youngest = _ - * .chain(users) - * .sortBy('age') - * .map(function(o) { - * return o.user + ' is ' + o.age; - * }) - * .head() - * .value(); - * // => 'pebbles is 1' - */ - function chain(value) { - var result = lodash(value); - result.__chain__ = true; - return result; - } - - /** - * This method invokes `interceptor` and returns `value`. The interceptor is - * invoked with one argument; (value). The purpose of this method is to "tap into" - * a method chain in order to perform operations on intermediate results within - * the chain. - * - * @static - * @memberOf _ - * @category Seq - * @param {*} value The value to provide to `interceptor`. - * @param {Function} interceptor The function to invoke. - * @returns {*} Returns `value`. - * @example - * - * _([1, 2, 3]) - * .tap(function(array) { - * array.pop(); - * }) - * .reverse() - * .value(); - * // => [2, 1] - */ - function tap(value, interceptor) { - interceptor(value); - return value; - } - - /** - * This method is like `_.tap` except that it returns the result of `interceptor`. - * - * @static - * @memberOf _ - * @category Seq - * @param {*} value The value to provide to `interceptor`. - * @param {Function} interceptor The function to invoke. - * @returns {*} Returns the result of `interceptor`. - * @example - * - * _(' abc ') - * .chain() - * .trim() - * .thru(function(value) { - * return [value]; - * }) - * .value(); - * // => ['abc'] - */ - function thru(value, interceptor) { - return interceptor(value); - } - - /** - * This method is the wrapper version of `_.at`. - * - * @name at - * @memberOf _ - * @category Seq - * @param {...(string|string[])} [paths] The property paths of elements to pick, - * specified individually or in arrays. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; - * - * _(object).at(['a[0].b.c', 'a[1]']).value(); - * // => [3, 4] - * - * _(['a', 'b', 'c']).at(0, 2).value(); - * // => ['a', 'c'] - */ - var wrapperAt = rest(function(paths) { - paths = baseFlatten(paths); - var length = paths.length, - start = length ? paths[0] : 0, - value = this.__wrapped__, - interceptor = function(object) { return baseAt(object, paths); }; - - if (length > 1 || this.__actions__.length || !(value instanceof LazyWrapper) || !isIndex(start)) { - return this.thru(interceptor); - } - value = value.slice(start, +start + (length ? 1 : 0)); - value.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined }); - return new LodashWrapper(value, this.__chain__).thru(function(array) { - if (length && !array.length) { - array.push(undefined); - } - return array; - }); - }); - - /** - * Enables explicit method chaining on the wrapper object. - * - * @name chain - * @memberOf _ - * @category Seq - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 40 } - * ]; - * - * // without explicit chaining - * _(users).head(); - * // => { 'user': 'barney', 'age': 36 } - * - * // with explicit chaining - * _(users) - * .chain() - * .head() - * .pick('user') - * .value(); - * // => { 'user': 'barney' } - */ - function wrapperChain() { - return chain(this); - } - - /** - * Executes the chained sequence and returns the wrapped result. - * - * @name commit - * @memberOf _ - * @category Seq - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var array = [1, 2]; - * var wrapped = _(array).push(3); - * - * console.log(array); - * // => [1, 2] - * - * wrapped = wrapped.commit(); - * console.log(array); - * // => [1, 2, 3] - * - * wrapped.last(); - * // => 3 - * - * console.log(array); - * // => [1, 2, 3] - */ - function wrapperCommit() { - return new LodashWrapper(this.value(), this.__chain__); - } - - /** - * This method is the wrapper version of `_.flatMap`. - * - * @static - * @memberOf _ - * @category Seq - * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * function duplicate(n) { - * return [n, n]; - * } - * - * _([1, 2]).flatMap(duplicate).value(); - * // => [1, 1, 2, 2] - */ - function wrapperFlatMap(iteratee) { - return this.map(iteratee).flatten(); - } - - /** - * Gets the next value on a wrapped object following the - * [iterator protocol](https://mdn.io/iteration_protocols#iterator). - * - * @name next - * @memberOf _ - * @category Seq - * @returns {Object} Returns the next iterator value. - * @example - * - * var wrapped = _([1, 2]); - * - * wrapped.next(); - * // => { 'done': false, 'value': 1 } - * - * wrapped.next(); - * // => { 'done': false, 'value': 2 } - * - * wrapped.next(); - * // => { 'done': true, 'value': undefined } - */ - function wrapperNext() { - if (this.__values__ === undefined) { - this.__values__ = toArray(this.value()); - } - var done = this.__index__ >= this.__values__.length, - value = done ? undefined : this.__values__[this.__index__++]; - - return { 'done': done, 'value': value }; - } - - /** - * Enables the wrapper to be iterable. - * - * @name Symbol.iterator - * @memberOf _ - * @category Seq - * @returns {Object} Returns the wrapper object. - * @example - * - * var wrapped = _([1, 2]); - * - * wrapped[Symbol.iterator]() === wrapped; - * // => true - * - * Array.from(wrapped); - * // => [1, 2] - */ - function wrapperToIterator() { - return this; - } - - /** - * Creates a clone of the chained sequence planting `value` as the wrapped value. - * - * @name plant - * @memberOf _ - * @category Seq - * @param {*} value The value to plant. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * function square(n) { - * return n * n; - * } - * - * var wrapped = _([1, 2]).map(square); - * var other = wrapped.plant([3, 4]); - * - * other.value(); - * // => [9, 16] - * - * wrapped.value(); - * // => [1, 4] - */ - function wrapperPlant(value) { - var result, - parent = this; - - while (parent instanceof baseLodash) { - var clone = wrapperClone(parent); - clone.__index__ = 0; - clone.__values__ = undefined; - if (result) { - previous.__wrapped__ = clone; - } else { - result = clone; - } - var previous = clone; - parent = parent.__wrapped__; - } - previous.__wrapped__ = value; - return result; - } - - /** - * This method is the wrapper version of `_.reverse`. - * - * **Note:** This method mutates the wrapped array. - * - * @name reverse - * @memberOf _ - * @category Seq - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var array = [1, 2, 3]; - * - * _(array).reverse().value() - * // => [3, 2, 1] - * - * console.log(array); - * // => [3, 2, 1] - */ - function wrapperReverse() { - var value = this.__wrapped__; - if (value instanceof LazyWrapper) { - var wrapped = value; - if (this.__actions__.length) { - wrapped = new LazyWrapper(this); - } - wrapped = wrapped.reverse(); - wrapped.__actions__.push({ 'func': thru, 'args': [reverse], 'thisArg': undefined }); - return new LodashWrapper(wrapped, this.__chain__); - } - return this.thru(reverse); - } - - /** - * Executes the chained sequence to extract the unwrapped value. - * - * @name value - * @memberOf _ - * @alias run, toJSON, valueOf - * @category Seq - * @returns {*} Returns the resolved unwrapped value. - * @example - * - * _([1, 2, 3]).value(); - * // => [1, 2, 3] - */ - function wrapperValue() { - return baseWrapperValue(this.__wrapped__, this.__actions__); - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` through `iteratee`. The corresponding value - * of each key is the number of times the key was returned by `iteratee`. - * The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.countBy([6.1, 4.2, 6.3], Math.floor); - * // => { '4': 1, '6': 2 } - * - * _.countBy(['one', 'two', 'three'], 'length'); - * // => { '3': 2, '5': 1 } - */ - var countBy = createAggregator(function(result, value, key) { - hasOwnProperty.call(result, key) ? ++result[key] : (result[key] = 1); - }); - - /** - * Checks if `predicate` returns truthy for **all** elements of `collection`. - * Iteration is stopped once `predicate` returns falsey. The predicate is - * invoked with three arguments: (value, index|key, collection). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {boolean} Returns `true` if all elements pass the predicate check, else `false`. - * @example - * - * _.every([true, 1, null, 'yes'], Boolean); - * // => false - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false } - * ]; - * - * // using the `_.matches` iteratee shorthand - * _.every(users, { 'user': 'barney', 'active': false }); - * // => false - * - * // using the `_.matchesProperty` iteratee shorthand - * _.every(users, ['active', false]); - * // => true - * - * // using the `_.property` iteratee shorthand - * _.every(users, 'active'); - * // => false - */ - function every(collection, predicate, guard) { - var func = isArray(collection) ? arrayEvery : baseEvery; - if (guard && isIterateeCall(collection, predicate, guard)) { - predicate = undefined; - } - return func(collection, getIteratee(predicate, 3)); - } - - /** - * Iterates over elements of `collection`, returning an array of all elements - * `predicate` returns truthy for. The predicate is invoked with three arguments: - * (value, index|key, collection). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': true }, - * { 'user': 'fred', 'age': 40, 'active': false } - * ]; - * - * _.filter(users, function(o) { return !o.active; }); - * // => objects for ['fred'] - * - * // using the `_.matches` iteratee shorthand - * _.filter(users, { 'age': 36, 'active': true }); - * // => objects for ['barney'] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.filter(users, ['active', false]); - * // => objects for ['fred'] - * - * // using the `_.property` iteratee shorthand - * _.filter(users, 'active'); - * // => objects for ['barney'] - */ - function filter(collection, predicate) { - var func = isArray(collection) ? arrayFilter : baseFilter; - return func(collection, getIteratee(predicate, 3)); - } - - /** - * Iterates over elements of `collection`, returning the first element - * `predicate` returns truthy for. The predicate is invoked with three arguments: - * (value, index|key, collection). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to search. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {*} Returns the matched element, else `undefined`. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': true }, - * { 'user': 'fred', 'age': 40, 'active': false }, - * { 'user': 'pebbles', 'age': 1, 'active': true } - * ]; - * - * _.find(users, function(o) { return o.age < 40; }); - * // => object for 'barney' - * - * // using the `_.matches` iteratee shorthand - * _.find(users, { 'age': 1, 'active': true }); - * // => object for 'pebbles' - * - * // using the `_.matchesProperty` iteratee shorthand - * _.find(users, ['active', false]); - * // => object for 'fred' - * - * // using the `_.property` iteratee shorthand - * _.find(users, 'active'); - * // => object for 'barney' - */ - function find(collection, predicate) { - predicate = getIteratee(predicate, 3); - if (isArray(collection)) { - var index = baseFindIndex(collection, predicate); - return index > -1 ? collection[index] : undefined; - } - return baseFind(collection, predicate, baseEach); - } - - /** - * This method is like `_.find` except that it iterates over elements of - * `collection` from right to left. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to search. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {*} Returns the matched element, else `undefined`. - * @example - * - * _.findLast([1, 2, 3, 4], function(n) { - * return n % 2 == 1; - * }); - * // => 3 - */ - function findLast(collection, predicate) { - predicate = getIteratee(predicate, 3); - if (isArray(collection)) { - var index = baseFindIndex(collection, predicate, true); - return index > -1 ? collection[index] : undefined; - } - return baseFind(collection, predicate, baseEachRight); - } - - /** - * Iterates over elements of `collection` invoking `iteratee` for each element. - * The iteratee is invoked with three arguments: (value, index|key, collection). - * Iteratee functions may exit iteration early by explicitly returning `false`. - * - * **Note:** As with other "Collections" methods, objects with a "length" property - * are iterated like arrays. To avoid this behavior use `_.forIn` or `_.forOwn` - * for object iteration. - * - * @static - * @memberOf _ - * @alias each - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - * @example - * - * _([1, 2]).forEach(function(value) { - * console.log(value); - * }); - * // => logs `1` then `2` - * - * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { - * console.log(key); - * }); - * // => logs 'a' then 'b' (iteration order is not guaranteed) - */ - function forEach(collection, iteratee) { - return (typeof iteratee == 'function' && isArray(collection)) - ? arrayEach(collection, iteratee) - : baseEach(collection, toFunction(iteratee)); - } - - /** - * This method is like `_.forEach` except that it iterates over elements of - * `collection` from right to left. - * - * @static - * @memberOf _ - * @alias eachRight - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - * @example - * - * _.forEachRight([1, 2], function(value) { - * console.log(value); - * }); - * // => logs `2` then `1` - */ - function forEachRight(collection, iteratee) { - return (typeof iteratee == 'function' && isArray(collection)) - ? arrayEachRight(collection, iteratee) - : baseEachRight(collection, toFunction(iteratee)); - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` through `iteratee`. The corresponding value - * of each key is an array of the elements responsible for generating the key. - * The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.groupBy([6.1, 4.2, 6.3], Math.floor); - * // => { '4': [4.2], '6': [6.1, 6.3] } - * - * // using the `_.property` iteratee shorthand - * _.groupBy(['one', 'two', 'three'], 'length'); - * // => { '3': ['one', 'two'], '5': ['three'] } - */ - var groupBy = createAggregator(function(result, value, key) { - if (hasOwnProperty.call(result, key)) { - result[key].push(value); - } else { - result[key] = [value]; - } - }); - - /** - * Checks if `value` is in `collection`. If `collection` is a string it's checked - * for a substring of `value`, otherwise [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * is used for equality comparisons. If `fromIndex` is negative, it's used as - * the offset from the end of `collection`. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object|string} collection The collection to search. - * @param {*} value The value to search for. - * @param {number} [fromIndex=0] The index to search from. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.reduce`. - * @returns {boolean} Returns `true` if `value` is found, else `false`. - * @example - * - * _.includes([1, 2, 3], 1); - * // => true - * - * _.includes([1, 2, 3], 1, 2); - * // => false - * - * _.includes({ 'user': 'fred', 'age': 40 }, 'fred'); - * // => true - * - * _.includes('pebbles', 'eb'); - * // => true - */ - function includes(collection, value, fromIndex, guard) { - collection = isArrayLike(collection) ? collection : values(collection); - fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0; - - var length = collection.length; - if (fromIndex < 0) { - fromIndex = nativeMax(length + fromIndex, 0); - } - return isString(collection) - ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1) - : (!!length && baseIndexOf(collection, value, fromIndex) > -1); - } - - /** - * Invokes the method at `path` of each element in `collection`, returning - * an array of the results of each invoked method. Any additional arguments - * are provided to each invoked method. If `methodName` is a function it's - * invoked for, and `this` bound to, each element in `collection`. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Array|Function|string} path The path of the method to invoke or - * the function invoked per iteration. - * @param {...*} [args] The arguments to invoke each method with. - * @returns {Array} Returns the array of results. - * @example - * - * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort'); - * // => [[1, 5, 7], [1, 2, 3]] - * - * _.invokeMap([123, 456], String.prototype.split, ''); - * // => [['1', '2', '3'], ['4', '5', '6']] - */ - var invokeMap = rest(function(collection, path, args) { - var index = -1, - isFunc = typeof path == 'function', - isProp = isKey(path), - result = isArrayLike(collection) ? Array(collection.length) : []; - - baseEach(collection, function(value) { - var func = isFunc ? path : ((isProp && value != null) ? value[path] : undefined); - result[++index] = func ? apply(func, value, args) : baseInvoke(value, path, args); - }); - return result; - }); - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` through `iteratee`. The corresponding value - * of each key is the last element responsible for generating the key. The - * iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * var keyData = [ - * { 'dir': 'left', 'code': 97 }, - * { 'dir': 'right', 'code': 100 } - * ]; - * - * _.keyBy(keyData, 'dir'); - * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } - * - * _.keyBy(keyData, function(o) { - * return String.fromCharCode(o.code); - * }); - * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } - */ - var keyBy = createAggregator(function(result, value, key) { - result[key] = value; - }); - - /** - * Creates an array of values by running each element in `collection` through - * `iteratee`. The iteratee is invoked with three arguments: - * (value, index|key, collection). - * - * Many lodash methods are guarded to work as iteratees for methods like - * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. - * - * The guarded methods are: - * `ary`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, `fill`, - * `invert`, `parseInt`, `random`, `range`, `rangeRight`, `slice`, `some`, - * `sortBy`, `take`, `takeRight`, `template`, `trim`, `trimEnd`, `trimStart`, - * and `words` - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - * @example - * - * function square(n) { - * return n * n; - * } - * - * _.map([1, 2], square); - * // => [3, 6] - * - * _.map({ 'a': 1, 'b': 2 }, square); - * // => [3, 6] (iteration order is not guaranteed) - * - * var users = [ - * { 'user': 'barney' }, - * { 'user': 'fred' } - * ]; - * - * // using the `_.property` iteratee shorthand - * _.map(users, 'user'); - * // => ['barney', 'fred'] - */ - function map(collection, iteratee) { - var func = isArray(collection) ? arrayMap : baseMap; - return func(collection, getIteratee(iteratee, 3)); - } - - /** - * This method is like `_.sortBy` except that it allows specifying the sort - * orders of the iteratees to sort by. If `orders` is unspecified, all values - * are sorted in ascending order. Otherwise, specify an order of "desc" for - * descending or "asc" for ascending sort order of corresponding values. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function[]|Object[]|string[]} [iteratees=[_.identity]] The iteratees to sort by. - * @param {string[]} [orders] The sort orders of `iteratees`. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.reduce`. - * @returns {Array} Returns the new sorted array. - * @example - * - * var users = [ - * { 'user': 'fred', 'age': 48 }, - * { 'user': 'barney', 'age': 34 }, - * { 'user': 'fred', 'age': 42 }, - * { 'user': 'barney', 'age': 36 } - * ]; - * - * // sort by `user` in ascending order and by `age` in descending order - * _.orderBy(users, ['user', 'age'], ['asc', 'desc']); - * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]] - */ - function orderBy(collection, iteratees, orders, guard) { - if (collection == null) { - return []; - } - if (!isArray(iteratees)) { - iteratees = iteratees == null ? [] : [iteratees]; - } - orders = guard ? undefined : orders; - if (!isArray(orders)) { - orders = orders == null ? [] : [orders]; - } - return baseOrderBy(collection, iteratees, orders); - } - - /** - * Creates an array of elements split into two groups, the first of which - * contains elements `predicate` returns truthy for, while the second of which - * contains elements `predicate` returns falsey for. The predicate is invoked - * with three arguments: (value, index|key, collection). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the array of grouped elements. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': false }, - * { 'user': 'fred', 'age': 40, 'active': true }, - * { 'user': 'pebbles', 'age': 1, 'active': false } - * ]; - * - * _.partition(users, function(o) { return o.active; }); - * // => objects for [['fred'], ['barney', 'pebbles']] - * - * // using the `_.matches` iteratee shorthand - * _.partition(users, { 'age': 1, 'active': false }); - * // => objects for [['pebbles'], ['barney', 'fred']] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.partition(users, ['active', false]); - * // => objects for [['barney', 'pebbles'], ['fred']] - * - * // using the `_.property` iteratee shorthand - * _.partition(users, 'active'); - * // => objects for [['fred'], ['barney', 'pebbles']] - */ - var partition = createAggregator(function(result, value, key) { - result[key ? 0 : 1].push(value); - }, function() { return [[], []]; }); - - /** - * Reduces `collection` to a value which is the accumulated result of running - * each element in `collection` through `iteratee`, where each successive - * invocation is supplied the return value of the previous. If `accumulator` - * is not provided the first element of `collection` is used as the initial - * value. The iteratee is invoked with four arguments: - * (accumulator, value, index|key, collection). - * - * Many lodash methods are guarded to work as iteratees for methods like - * `_.reduce`, `_.reduceRight`, and `_.transform`. - * - * The guarded methods are: - * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`, - * and `sortBy` - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @returns {*} Returns the accumulated value. - * @example - * - * _.reduce([1, 2], function(sum, n) { - * return sum + n; - * }); - * // => 3 - * - * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { - * (result[value] || (result[value] = [])).push(key); - * return result; - * }, {}); - * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed) - */ - function reduce(collection, iteratee, accumulator) { - var func = isArray(collection) ? arrayReduce : baseReduce, - initFromCollection = arguments.length < 3; - - return func(collection, getIteratee(iteratee, 4), accumulator, initFromCollection, baseEach); - } - - /** - * This method is like `_.reduce` except that it iterates over elements of - * `collection` from right to left. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @returns {*} Returns the accumulated value. - * @example - * - * var array = [[0, 1], [2, 3], [4, 5]]; - * - * _.reduceRight(array, function(flattened, other) { - * return flattened.concat(other); - * }, []); - * // => [4, 5, 2, 3, 0, 1] - */ - function reduceRight(collection, iteratee, accumulator) { - var func = isArray(collection) ? arrayReduceRight : baseReduce, - initFromCollection = arguments.length < 3; - - return func(collection, getIteratee(iteratee, 4), accumulator, initFromCollection, baseEachRight); - } - - /** - * The opposite of `_.filter`; this method returns the elements of `collection` - * that `predicate` does **not** return truthy for. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': false }, - * { 'user': 'fred', 'age': 40, 'active': true } - * ]; - * - * _.reject(users, function(o) { return !o.active; }); - * // => objects for ['fred'] - * - * // using the `_.matches` iteratee shorthand - * _.reject(users, { 'age': 40, 'active': true }); - * // => objects for ['barney'] - * - * // using the `_.matchesProperty` iteratee shorthand - * _.reject(users, ['active', false]); - * // => objects for ['fred'] - * - * // using the `_.property` iteratee shorthand - * _.reject(users, 'active'); - * // => objects for ['barney'] - */ - function reject(collection, predicate) { - var func = isArray(collection) ? arrayFilter : baseFilter; - predicate = getIteratee(predicate, 3); - return func(collection, function(value, index, collection) { - return !predicate(value, index, collection); - }); - } - - /** - * Gets a random element from `collection`. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to sample. - * @returns {*} Returns the random element. - * @example - * - * _.sample([1, 2, 3, 4]); - * // => 2 - */ - function sample(collection) { - var array = isArrayLike(collection) ? collection : values(collection), - length = array.length; - - return length > 0 ? array[baseRandom(0, length - 1)] : undefined; - } - - /** - * Gets `n` random elements from `collection`. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to sample. - * @param {number} [n=0] The number of elements to sample. - * @returns {Array} Returns the random elements. - * @example - * - * _.sampleSize([1, 2, 3, 4], 2); - * // => [3, 1] - */ - function sampleSize(collection, n) { - var index = -1, - result = toArray(collection), - length = result.length, - lastIndex = length - 1; - - n = baseClamp(toInteger(n), 0, length); - while (++index < n) { - var rand = baseRandom(index, lastIndex), - value = result[rand]; - - result[rand] = result[index]; - result[index] = value; - } - result.length = n; - return result; - } - - /** - * Creates an array of shuffled values, using a version of the - * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to shuffle. - * @returns {Array} Returns the new shuffled array. - * @example - * - * _.shuffle([1, 2, 3, 4]); - * // => [4, 1, 3, 2] - */ - function shuffle(collection) { - return sampleSize(collection, MAX_ARRAY_LENGTH); - } - - /** - * Gets the size of `collection` by returning its length for array-like - * values or the number of own enumerable properties for objects. - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to inspect. - * @returns {number} Returns the collection size. - * @example - * - * _.size([1, 2, 3]); - * // => 3 - * - * _.size({ 'a': 1, 'b': 2 }); - * // => 2 - * - * _.size('pebbles'); - * // => 7 - */ - function size(collection) { - if (collection == null) { - return 0; - } - if (isArrayLike(collection)) { - var result = collection.length; - return (result && isString(collection)) ? stringSize(collection) : result; - } - return keys(collection).length; - } - - /** - * Checks if `predicate` returns truthy for **any** element of `collection`. - * Iteration is stopped once `predicate` returns truthy. The predicate is - * invoked with three arguments: (value, index|key, collection). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {boolean} Returns `true` if any element passes the predicate check, else `false`. - * @example - * - * _.some([null, 0, 'yes', false], Boolean); - * // => true - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false } - * ]; - * - * // using the `_.matches` iteratee shorthand - * _.some(users, { 'user': 'barney', 'active': false }); - * // => false - * - * // using the `_.matchesProperty` iteratee shorthand - * _.some(users, ['active', false]); - * // => true - * - * // using the `_.property` iteratee shorthand - * _.some(users, 'active'); - * // => true - */ - function some(collection, predicate, guard) { - var func = isArray(collection) ? arraySome : baseSome; - if (guard && isIterateeCall(collection, predicate, guard)) { - predicate = undefined; - } - return func(collection, getIteratee(predicate, 3)); - } - - /** - * Creates an array of elements, sorted in ascending order by the results of - * running each element in a collection through each iteratee. This method - * performs a stable sort, that is, it preserves the original sort order of - * equal elements. The iteratees are invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {...(Function|Function[]|Object|Object[]|string|string[])} [iteratees=[_.identity]] - * The iteratees to sort by, specified individually or in arrays. - * @returns {Array} Returns the new sorted array. - * @example - * - * var users = [ - * { 'user': 'fred', 'age': 48 }, - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 42 }, - * { 'user': 'barney', 'age': 34 } - * ]; - * - * _.sortBy(users, function(o) { return o.user; }); - * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]] - * - * _.sortBy(users, ['user', 'age']); - * // => objects for [['barney', 34], ['barney', 36], ['fred', 42], ['fred', 48]] - * - * _.sortBy(users, 'user', function(o) { - * return Math.floor(o.age / 10); - * }); - * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]] - */ - var sortBy = rest(function(collection, iteratees) { - if (collection == null) { - return []; - } - var length = iteratees.length; - if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) { - iteratees = []; - } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) { - iteratees.length = 1; - } - return baseOrderBy(collection, baseFlatten(iteratees), []); - }); - - /*------------------------------------------------------------------------*/ - - /** - * Gets the timestamp of the number of milliseconds that have elapsed since - * the Unix epoch (1 January 1970 00:00:00 UTC). - * - * @static - * @memberOf _ - * @type Function - * @category Date - * @returns {number} Returns the timestamp. - * @example - * - * _.defer(function(stamp) { - * console.log(_.now() - stamp); - * }, _.now()); - * // => logs the number of milliseconds it took for the deferred function to be invoked - */ - var now = Date.now; - - /*------------------------------------------------------------------------*/ - - /** - * The opposite of `_.before`; this method creates a function that invokes - * `func` once it's called `n` or more times. - * - * @static - * @memberOf _ - * @category Function - * @param {number} n The number of calls before `func` is invoked. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var saves = ['profile', 'settings']; - * - * var done = _.after(saves.length, function() { - * console.log('done saving!'); - * }); - * - * _.forEach(saves, function(type) { - * asyncSave({ 'type': type, 'complete': done }); - * }); - * // => logs 'done saving!' after the two async saves have completed - */ - function after(n, func) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - n = toInteger(n); - return function() { - if (--n < 1) { - return func.apply(this, arguments); - } - }; - } - - /** - * Creates a function that accepts up to `n` arguments, ignoring any - * additional arguments. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to cap arguments for. - * @param {number} [n=func.length] The arity cap. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Function} Returns the new function. - * @example - * - * _.map(['6', '8', '10'], _.ary(parseInt, 1)); - * // => [6, 8, 10] - */ - function ary(func, n, guard) { - n = guard ? undefined : n; - n = (func && n == null) ? func.length : n; - return createWrapper(func, ARY_FLAG, undefined, undefined, undefined, undefined, n); - } - - /** - * Creates a function that invokes `func`, with the `this` binding and arguments - * of the created function, while it's called less than `n` times. Subsequent - * calls to the created function return the result of the last `func` invocation. - * - * @static - * @memberOf _ - * @category Function - * @param {number} n The number of calls at which `func` is no longer invoked. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * jQuery(element).on('click', _.before(5, addContactToList)); - * // => allows adding up to 4 contacts to the list - */ - function before(n, func) { - var result; - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - n = toInteger(n); - return function() { - if (--n > 0) { - result = func.apply(this, arguments); - } - if (n <= 1) { - func = undefined; - } - return result; - }; - } - - /** - * Creates a function that invokes `func` with the `this` binding of `thisArg` - * and prepends any additional `_.bind` arguments to those provided to the - * bound function. - * - * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, - * may be used as a placeholder for partially applied arguments. - * - * **Note:** Unlike native `Function#bind` this method doesn't set the "length" - * property of bound functions. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to bind. - * @param {*} thisArg The `this` binding of `func`. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var greet = function(greeting, punctuation) { - * return greeting + ' ' + this.user + punctuation; - * }; - * - * var object = { 'user': 'fred' }; - * - * var bound = _.bind(greet, object, 'hi'); - * bound('!'); - * // => 'hi fred!' - * - * // using placeholders - * var bound = _.bind(greet, object, _, '!'); - * bound('hi'); - * // => 'hi fred!' - */ - var bind = rest(function(func, thisArg, partials) { - var bitmask = BIND_FLAG; - if (partials.length) { - var holders = replaceHolders(partials, bind.placeholder); - bitmask |= PARTIAL_FLAG; - } - return createWrapper(func, bitmask, thisArg, partials, holders); - }); - - /** - * Creates a function that invokes the method at `object[key]` and prepends - * any additional `_.bindKey` arguments to those provided to the bound function. - * - * This method differs from `_.bind` by allowing bound functions to reference - * methods that may be redefined or don't yet exist. - * See [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern) - * for more details. - * - * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for partially applied arguments. - * - * @static - * @memberOf _ - * @category Function - * @param {Object} object The object to invoke the method on. - * @param {string} key The key of the method. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var object = { - * 'user': 'fred', - * 'greet': function(greeting, punctuation) { - * return greeting + ' ' + this.user + punctuation; - * } - * }; - * - * var bound = _.bindKey(object, 'greet', 'hi'); - * bound('!'); - * // => 'hi fred!' - * - * object.greet = function(greeting, punctuation) { - * return greeting + 'ya ' + this.user + punctuation; - * }; - * - * bound('!'); - * // => 'hiya fred!' - * - * // using placeholders - * var bound = _.bindKey(object, 'greet', _, '!'); - * bound('hi'); - * // => 'hiya fred!' - */ - var bindKey = rest(function(object, key, partials) { - var bitmask = BIND_FLAG | BIND_KEY_FLAG; - if (partials.length) { - var holders = replaceHolders(partials, bindKey.placeholder); - bitmask |= PARTIAL_FLAG; - } - return createWrapper(key, bitmask, object, partials, holders); - }); - - /** - * Creates a function that accepts arguments of `func` and either invokes - * `func` returning its result, if at least `arity` number of arguments have - * been provided, or returns a function that accepts the remaining `func` - * arguments, and so on. The arity of `func` may be specified if `func.length` - * is not sufficient. - * - * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, - * may be used as a placeholder for provided arguments. - * - * **Note:** This method doesn't set the "length" property of curried functions. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to curry. - * @param {number} [arity=func.length] The arity of `func`. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Function} Returns the new curried function. - * @example - * - * var abc = function(a, b, c) { - * return [a, b, c]; - * }; - * - * var curried = _.curry(abc); - * - * curried(1)(2)(3); - * // => [1, 2, 3] - * - * curried(1, 2)(3); - * // => [1, 2, 3] - * - * curried(1, 2, 3); - * // => [1, 2, 3] - * - * // using placeholders - * curried(1)(_, 3)(2); - * // => [1, 2, 3] - */ - function curry(func, arity, guard) { - arity = guard ? undefined : arity; - var result = createWrapper(func, CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); - result.placeholder = curry.placeholder; - return result; - } - - /** - * This method is like `_.curry` except that arguments are applied to `func` - * in the manner of `_.partialRight` instead of `_.partial`. - * - * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for provided arguments. - * - * **Note:** This method doesn't set the "length" property of curried functions. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to curry. - * @param {number} [arity=func.length] The arity of `func`. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Function} Returns the new curried function. - * @example - * - * var abc = function(a, b, c) { - * return [a, b, c]; - * }; - * - * var curried = _.curryRight(abc); - * - * curried(3)(2)(1); - * // => [1, 2, 3] - * - * curried(2, 3)(1); - * // => [1, 2, 3] - * - * curried(1, 2, 3); - * // => [1, 2, 3] - * - * // using placeholders - * curried(3)(1, _)(2); - * // => [1, 2, 3] - */ - function curryRight(func, arity, guard) { - arity = guard ? undefined : arity; - var result = createWrapper(func, CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity); - result.placeholder = curryRight.placeholder; - return result; - } - - /** - * Creates a debounced function that delays invoking `func` until after `wait` - * milliseconds have elapsed since the last time the debounced function was - * invoked. The debounced function comes with a `cancel` method to cancel - * delayed `func` invocations and a `flush` method to immediately invoke them. - * Provide an options object to indicate whether `func` should be invoked on - * the leading and/or trailing edge of the `wait` timeout. The `func` is invoked - * with the last arguments provided to the debounced function. Subsequent calls - * to the debounced function return the result of the last `func` invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked - * on the trailing edge of the timeout only if the the debounced function is - * invoked more than once during the `wait` timeout. - * - * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) - * for details over the differences between `_.debounce` and `_.throttle`. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to debounce. - * @param {number} [wait=0] The number of milliseconds to delay. - * @param {Object} [options] The options object. - * @param {boolean} [options.leading=false] Specify invoking on the leading - * edge of the timeout. - * @param {number} [options.maxWait] The maximum time `func` is allowed to be - * delayed before it's invoked. - * @param {boolean} [options.trailing=true] Specify invoking on the trailing - * edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // avoid costly calculations while the window size is in flux - * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); - * - * // invoke `sendMail` when clicked, debouncing subsequent calls - * jQuery(element).on('click', _.debounce(sendMail, 300, { - * 'leading': true, - * 'trailing': false - * })); - * - * // ensure `batchLog` is invoked once after 1 second of debounced calls - * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); - * var source = new EventSource('/stream'); - * jQuery(source).on('message', debounced); - * - * // cancel a trailing debounced invocation - * jQuery(window).on('popstate', debounced.cancel); - */ - function debounce(func, wait, options) { - var args, - maxTimeoutId, - result, - stamp, - thisArg, - timeoutId, - trailingCall, - lastCalled = 0, - leading = false, - maxWait = false, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - wait = toNumber(wait) || 0; - if (isObject(options)) { - leading = !!options.leading; - maxWait = 'maxWait' in options && nativeMax(toNumber(options.maxWait) || 0, wait); - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - - function cancel() { - if (timeoutId) { - clearTimeout(timeoutId); - } - if (maxTimeoutId) { - clearTimeout(maxTimeoutId); - } - lastCalled = 0; - args = maxTimeoutId = thisArg = timeoutId = trailingCall = undefined; - } - - function complete(isCalled, id) { - if (id) { - clearTimeout(id); - } - maxTimeoutId = timeoutId = trailingCall = undefined; - if (isCalled) { - lastCalled = now(); - result = func.apply(thisArg, args); - if (!timeoutId && !maxTimeoutId) { - args = thisArg = undefined; - } - } - } - - function delayed() { - var remaining = wait - (now() - stamp); - if (remaining <= 0 || remaining > wait) { - complete(trailingCall, maxTimeoutId); - } else { - timeoutId = setTimeout(delayed, remaining); - } - } - - function flush() { - if ((timeoutId && trailingCall) || (maxTimeoutId && trailing)) { - result = func.apply(thisArg, args); - } - cancel(); - return result; - } - - function maxDelayed() { - complete(trailing, timeoutId); - } - - function debounced() { - args = arguments; - stamp = now(); - thisArg = this; - trailingCall = trailing && (timeoutId || !leading); - - if (maxWait === false) { - var leadingCall = leading && !timeoutId; - } else { - if (!maxTimeoutId && !leading) { - lastCalled = stamp; - } - var remaining = maxWait - (stamp - lastCalled), - isCalled = remaining <= 0 || remaining > maxWait; - - if (isCalled) { - if (maxTimeoutId) { - maxTimeoutId = clearTimeout(maxTimeoutId); - } - lastCalled = stamp; - result = func.apply(thisArg, args); - } - else if (!maxTimeoutId) { - maxTimeoutId = setTimeout(maxDelayed, remaining); - } - } - if (isCalled && timeoutId) { - timeoutId = clearTimeout(timeoutId); - } - else if (!timeoutId && wait !== maxWait) { - timeoutId = setTimeout(delayed, wait); - } - if (leadingCall) { - isCalled = true; - result = func.apply(thisArg, args); - } - if (isCalled && !timeoutId && !maxTimeoutId) { - args = thisArg = undefined; - } - return result; - } - debounced.cancel = cancel; - debounced.flush = flush; - return debounced; - } - - /** - * Defers invoking the `func` until the current call stack has cleared. Any - * additional arguments are provided to `func` when it's invoked. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to defer. - * @param {...*} [args] The arguments to invoke `func` with. - * @returns {number} Returns the timer id. - * @example - * - * _.defer(function(text) { - * console.log(text); - * }, 'deferred'); - * // logs 'deferred' after one or more milliseconds - */ - var defer = rest(function(func, args) { - return baseDelay(func, 1, args); - }); - - /** - * Invokes `func` after `wait` milliseconds. Any additional arguments are - * provided to `func` when it's invoked. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to delay. - * @param {number} wait The number of milliseconds to delay invocation. - * @param {...*} [args] The arguments to invoke `func` with. - * @returns {number} Returns the timer id. - * @example - * - * _.delay(function(text) { - * console.log(text); - * }, 1000, 'later'); - * // => logs 'later' after one second - */ - var delay = rest(function(func, wait, args) { - return baseDelay(func, toNumber(wait) || 0, args); - }); - - /** - * Creates a function that invokes `func` with arguments reversed. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to flip arguments for. - * @returns {Function} Returns the new function. - * @example - * - * var flipped = _.flip(function() { - * return _.toArray(arguments); - * }); - * - * flipped('a', 'b', 'c', 'd'); - * // => ['d', 'c', 'b', 'a'] - */ - function flip(func) { - return createWrapper(func, FLIP_FLAG); - } - - /** - * Creates a function that memoizes the result of `func`. If `resolver` is - * provided it determines the cache key for storing the result based on the - * arguments provided to the memoized function. By default, the first argument - * provided to the memoized function is used as the map cache key. The `func` - * is invoked with the `this` binding of the memoized function. - * - * **Note:** The cache is exposed as the `cache` property on the memoized - * function. Its creation may be customized by replacing the `_.memoize.Cache` - * constructor with one whose instances implement the [`Map`](http://ecma-international.org/ecma-262/6.0/#sec-properties-of-the-map-prototype-object) - * method interface of `delete`, `get`, `has`, and `set`. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to have its output memoized. - * @param {Function} [resolver] The function to resolve the cache key. - * @returns {Function} Returns the new memoizing function. - * @example - * - * var object = { 'a': 1, 'b': 2 }; - * var other = { 'c': 3, 'd': 4 }; - * - * var values = _.memoize(_.values); - * values(object); - * // => [1, 2] - * - * values(other); - * // => [3, 4] - * - * object.a = 2; - * values(object); - * // => [1, 2] - * - * // modifying the result cache - * values.cache.set(object, ['a', 'b']); - * values(object); - * // => ['a', 'b'] - * - * // replacing `_.memoize.Cache` - * _.memoize.Cache = WeakMap; - */ - function memoize(func, resolver) { - if (typeof func != 'function' || (resolver && typeof resolver != 'function')) { - throw new TypeError(FUNC_ERROR_TEXT); - } - var memoized = function() { - var args = arguments, - key = resolver ? resolver.apply(this, args) : args[0], - cache = memoized.cache; - - if (cache.has(key)) { - return cache.get(key); - } - var result = func.apply(this, args); - memoized.cache = cache.set(key, result); - return result; - }; - memoized.cache = new memoize.Cache; - return memoized; - } - - /** - * Creates a function that negates the result of the predicate `func`. The - * `func` predicate is invoked with the `this` binding and arguments of the - * created function. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} predicate The predicate to negate. - * @returns {Function} Returns the new function. - * @example - * - * function isEven(n) { - * return n % 2 == 0; - * } - * - * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); - * // => [1, 3, 5] - */ - function negate(predicate) { - if (typeof predicate != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - return function() { - return !predicate.apply(this, arguments); - }; - } - - /** - * Creates a function that is restricted to invoking `func` once. Repeat calls - * to the function return the value of the first invocation. The `func` is - * invoked with the `this` binding and arguments of the created function. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var initialize = _.once(createApplication); - * initialize(); - * initialize(); - * // `initialize` invokes `createApplication` once - */ - function once(func) { - return before(2, func); - } - - /** - * Creates a function that invokes `func` with arguments transformed by - * corresponding `transforms`. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to wrap. - * @param {...(Function|Function[])} [transforms] The functions to transform - * arguments, specified individually or in arrays. - * @returns {Function} Returns the new function. - * @example - * - * function doubled(n) { - * return n * 2; - * } - * - * function square(n) { - * return n * n; - * } - * - * var func = _.overArgs(function(x, y) { - * return [x, y]; - * }, square, doubled); - * - * func(9, 3); - * // => [81, 6] - * - * func(10, 5); - * // => [100, 10] - */ - var overArgs = rest(function(func, transforms) { - transforms = arrayMap(baseFlatten(transforms), getIteratee()); - - var funcsLength = transforms.length; - return rest(function(args) { - var index = -1, - length = nativeMin(args.length, funcsLength); - - while (++index < length) { - args[index] = transforms[index].call(this, args[index]); - } - return apply(func, this, args); - }); - }); - - /** - * Creates a function that invokes `func` with `partial` arguments prepended - * to those provided to the new function. This method is like `_.bind` except - * it does **not** alter the `this` binding. - * - * The `_.partial.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for partially applied arguments. - * - * **Note:** This method doesn't set the "length" property of partially - * applied functions. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to partially apply arguments to. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * var greet = function(greeting, name) { - * return greeting + ' ' + name; - * }; - * - * var sayHelloTo = _.partial(greet, 'hello'); - * sayHelloTo('fred'); - * // => 'hello fred' - * - * // using placeholders - * var greetFred = _.partial(greet, _, 'fred'); - * greetFred('hi'); - * // => 'hi fred' - */ - var partial = rest(function(func, partials) { - var holders = replaceHolders(partials, partial.placeholder); - return createWrapper(func, PARTIAL_FLAG, undefined, partials, holders); - }); - - /** - * This method is like `_.partial` except that partially applied arguments - * are appended to those provided to the new function. - * - * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for partially applied arguments. - * - * **Note:** This method doesn't set the "length" property of partially - * applied functions. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to partially apply arguments to. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * var greet = function(greeting, name) { - * return greeting + ' ' + name; - * }; - * - * var greetFred = _.partialRight(greet, 'fred'); - * greetFred('hi'); - * // => 'hi fred' - * - * // using placeholders - * var sayHelloTo = _.partialRight(greet, 'hello', _); - * sayHelloTo('fred'); - * // => 'hello fred' - */ - var partialRight = rest(function(func, partials) { - var holders = replaceHolders(partials, partialRight.placeholder); - return createWrapper(func, PARTIAL_RIGHT_FLAG, undefined, partials, holders); - }); - - /** - * Creates a function that invokes `func` with arguments arranged according - * to the specified indexes where the argument value at the first index is - * provided as the first argument, the argument value at the second index is - * provided as the second argument, and so on. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to rearrange arguments for. - * @param {...(number|number[])} indexes The arranged argument indexes, - * specified individually or in arrays. - * @returns {Function} Returns the new function. - * @example - * - * var rearged = _.rearg(function(a, b, c) { - * return [a, b, c]; - * }, 2, 0, 1); - * - * rearged('b', 'c', 'a') - * // => ['a', 'b', 'c'] - */ - var rearg = rest(function(func, indexes) { - return createWrapper(func, REARG_FLAG, undefined, undefined, undefined, baseFlatten(indexes)); - }); - - /** - * Creates a function that invokes `func` with the `this` binding of the - * created function and arguments from `start` and beyond provided as an array. - * - * **Note:** This method is based on the [rest parameter](https://mdn.io/rest_parameters). - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to apply a rest parameter to. - * @param {number} [start=func.length-1] The start position of the rest parameter. - * @returns {Function} Returns the new function. - * @example - * - * var say = _.rest(function(what, names) { - * return what + ' ' + _.initial(names).join(', ') + - * (_.size(names) > 1 ? ', & ' : '') + _.last(names); - * }); - * - * say('hello', 'fred', 'barney', 'pebbles'); - * // => 'hello fred, barney, & pebbles' - */ - function rest(func, start) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - start = nativeMax(start === undefined ? (func.length - 1) : toInteger(start), 0); - return function() { - var args = arguments, - index = -1, - length = nativeMax(args.length - start, 0), - array = Array(length); - - while (++index < length) { - array[index] = args[start + index]; - } - switch (start) { - case 0: return func.call(this, array); - case 1: return func.call(this, args[0], array); - case 2: return func.call(this, args[0], args[1], array); - } - var otherArgs = Array(start + 1); - index = -1; - while (++index < start) { - otherArgs[index] = args[index]; - } - otherArgs[start] = array; - return apply(func, this, otherArgs); - }; - } - - /** - * Creates a function that invokes `func` with the `this` binding of the created - * function and an array of arguments much like [`Function#apply`](https://es5.github.io/#x15.3.4.3). - * - * **Note:** This method is based on the [spread operator](https://mdn.io/spread_operator). - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to spread arguments over. - * @returns {Function} Returns the new function. - * @example - * - * var say = _.spread(function(who, what) { - * return who + ' says ' + what; - * }); - * - * say(['fred', 'hello']); - * // => 'fred says hello' - * - * // with a Promise - * var numbers = Promise.all([ - * Promise.resolve(40), - * Promise.resolve(36) - * ]); - * - * numbers.then(_.spread(function(x, y) { - * return x + y; - * })); - * // => a Promise of 76 - */ - function spread(func) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - return function(array) { - return apply(func, this, array); - }; - } - - /** - * Creates a throttled function that only invokes `func` at most once per - * every `wait` milliseconds. The throttled function comes with a `cancel` - * method to cancel delayed `func` invocations and a `flush` method to - * immediately invoke them. Provide an options object to indicate whether - * `func` should be invoked on the leading and/or trailing edge of the `wait` - * timeout. The `func` is invoked with the last arguments provided to the - * throttled function. Subsequent calls to the throttled function return the - * result of the last `func` invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked - * on the trailing edge of the timeout only if the the throttled function is - * invoked more than once during the `wait` timeout. - * - * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) - * for details over the differences between `_.throttle` and `_.debounce`. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to throttle. - * @param {number} [wait=0] The number of milliseconds to throttle invocations to. - * @param {Object} [options] The options object. - * @param {boolean} [options.leading=true] Specify invoking on the leading - * edge of the timeout. - * @param {boolean} [options.trailing=true] Specify invoking on the trailing - * edge of the timeout. - * @returns {Function} Returns the new throttled function. - * @example - * - * // avoid excessively updating the position while scrolling - * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); - * - * // invoke `renewToken` when the click event is fired, but not more than once every 5 minutes - * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); - * jQuery(element).on('click', throttled); - * - * // cancel a trailing throttled invocation - * jQuery(window).on('popstate', throttled.cancel); - */ - function throttle(func, wait, options) { - var leading = true, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - if (isObject(options)) { - leading = 'leading' in options ? !!options.leading : leading; - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - return debounce(func, wait, { 'leading': leading, 'maxWait': wait, 'trailing': trailing }); - } - - /** - * Creates a function that accepts up to one argument, ignoring any - * additional arguments. - * - * @static - * @memberOf _ - * @category Function - * @param {Function} func The function to cap arguments for. - * @returns {Function} Returns the new function. - * @example - * - * _.map(['6', '8', '10'], _.unary(parseInt)); - * // => [6, 8, 10] - */ - function unary(func) { - return ary(func, 1); - } - - /** - * Creates a function that provides `value` to the wrapper function as its - * first argument. Any additional arguments provided to the function are - * appended to those provided to the wrapper function. The wrapper is invoked - * with the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Function - * @param {*} value The value to wrap. - * @param {Function} wrapper The wrapper function. - * @returns {Function} Returns the new function. - * @example - * - * var p = _.wrap(_.escape, function(func, text) { - * return '

      ' + func(text) + '

      '; - * }); - * - * p('fred, barney, & pebbles'); - * // => '

      fred, barney, & pebbles

      ' - */ - function wrap(value, wrapper) { - wrapper = wrapper == null ? identity : wrapper; - return partial(wrapper, value); - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates a shallow clone of `value`. - * - * **Note:** This method is loosely based on the - * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) - * and supports cloning arrays, array buffers, booleans, date objects, maps, - * numbers, `Object` objects, regexes, sets, strings, symbols, and typed - * arrays. The own enumerable properties of `arguments` objects are cloned - * as plain objects. An empty object is returned for uncloneable values such - * as error objects, functions, DOM nodes, and WeakMaps. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to clone. - * @returns {*} Returns the cloned value. - * @example - * - * var objects = [{ 'a': 1 }, { 'b': 2 }]; - * - * var shallow = _.clone(objects); - * console.log(shallow[0] === objects[0]); - * // => true - */ - function clone(value) { - return baseClone(value); - } - - /** - * This method is like `_.clone` except that it accepts `customizer` which - * is invoked to produce the cloned value. If `customizer` returns `undefined` - * cloning is handled by the method instead. The `customizer` is invoked with - * up to five arguments; (value [, index|key, object, stack]). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to clone. - * @param {Function} [customizer] The function to customize cloning. - * @returns {*} Returns the cloned value. - * @example - * - * function customizer(value) { - * if (_.isElement(value)) { - * return value.cloneNode(false); - * } - * } - * - * var el = _.clone(document.body, customizer); - * - * console.log(el === document.body); - * // => false - * console.log(el.nodeName); - * // => 'BODY' - * console.log(el.childNodes.length); - * // => 0 - */ - function cloneWith(value, customizer) { - return baseClone(value, false, customizer); - } - - /** - * This method is like `_.clone` except that it recursively clones `value`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to recursively clone. - * @returns {*} Returns the deep cloned value. - * @example - * - * var objects = [{ 'a': 1 }, { 'b': 2 }]; - * - * var deep = _.cloneDeep(objects); - * console.log(deep[0] === objects[0]); - * // => false - */ - function cloneDeep(value) { - return baseClone(value, true); - } - - /** - * This method is like `_.cloneWith` except that it recursively clones `value`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to recursively clone. - * @param {Function} [customizer] The function to customize cloning. - * @returns {*} Returns the deep cloned value. - * @example - * - * function customizer(value) { - * if (_.isElement(value)) { - * return value.cloneNode(true); - * } - * } - * - * var el = _.cloneDeep(document.body, customizer); - * - * console.log(el === document.body); - * // => false - * console.log(el.nodeName); - * // => 'BODY' - * console.log(el.childNodes.length); - * // => 20 - */ - function cloneDeepWith(value, customizer) { - return baseClone(value, true, customizer); - } - - /** - * Performs a [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - * comparison between two values to determine if they are equivalent. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var object = { 'user': 'fred' }; - * var other = { 'user': 'fred' }; - * - * _.eq(object, object); - * // => true - * - * _.eq(object, other); - * // => false - * - * _.eq('a', 'a'); - * // => true - * - * _.eq('a', Object('a')); - * // => false - * - * _.eq(NaN, NaN); - * // => true - */ - function eq(value, other) { - return value === other || (value !== value && other !== other); - } - - /** - * Checks if `value` is greater than `other`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is greater than `other`, else `false`. - * @example - * - * _.gt(3, 1); - * // => true - * - * _.gt(3, 3); - * // => false - * - * _.gt(1, 3); - * // => false - */ - function gt(value, other) { - return value > other; - } - - /** - * Checks if `value` is greater than or equal to `other`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is greater than or equal to `other`, else `false`. - * @example - * - * _.gte(3, 1); - * // => true - * - * _.gte(3, 3); - * // => true - * - * _.gte(1, 3); - * // => false - */ - function gte(value, other) { - return value >= other; - } - - /** - * Checks if `value` is likely an `arguments` object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isArguments(function() { return arguments; }()); - * // => true - * - * _.isArguments([1, 2, 3]); - * // => false - */ - function isArguments(value) { - // Safari 8.1 incorrectly makes `arguments.callee` enumerable in strict mode. - return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') && - (!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag); - } - - /** - * Checks if `value` is classified as an `Array` object. - * - * @static - * @memberOf _ - * @type Function - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isArray([1, 2, 3]); - * // => true - * - * _.isArray(document.body.children); - * // => false - * - * _.isArray('abc'); - * // => false - * - * _.isArray(_.noop); - * // => false - */ - var isArray = Array.isArray; - - /** - * Checks if `value` is array-like. A value is considered array-like if it's - * not a function and has a `value.length` that's an integer greater than or - * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. - * - * @static - * @memberOf _ - * @type Function - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is array-like, else `false`. - * @example - * - * _.isArrayLike([1, 2, 3]); - * // => true - * - * _.isArrayLike(document.body.children); - * // => true - * - * _.isArrayLike('abc'); - * // => true - * - * _.isArrayLike(_.noop); - * // => false - */ - function isArrayLike(value) { - return value != null && - !(typeof value == 'function' && isFunction(value)) && isLength(getLength(value)); - } - - /** - * This method is like `_.isArrayLike` except that it also checks if `value` - * is an object. - * - * @static - * @memberOf _ - * @type Function - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an array-like object, else `false`. - * @example - * - * _.isArrayLikeObject([1, 2, 3]); - * // => true - * - * _.isArrayLikeObject(document.body.children); - * // => true - * - * _.isArrayLikeObject('abc'); - * // => false - * - * _.isArrayLikeObject(_.noop); - * // => false - */ - function isArrayLikeObject(value) { - return isObjectLike(value) && isArrayLike(value); - } - - /** - * Checks if `value` is classified as a boolean primitive or object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isBoolean(false); - * // => true - * - * _.isBoolean(null); - * // => false - */ - function isBoolean(value) { - return value === true || value === false || - (isObjectLike(value) && objectToString.call(value) == boolTag); - } - - /** - * Checks if `value` is classified as a `Date` object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isDate(new Date); - * // => true - * - * _.isDate('Mon April 23 2012'); - * // => false - */ - function isDate(value) { - return isObjectLike(value) && objectToString.call(value) == dateTag; - } - - /** - * Checks if `value` is likely a DOM element. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`. - * @example - * - * _.isElement(document.body); - * // => true - * - * _.isElement(''); - * // => false - */ - function isElement(value) { - return !!value && value.nodeType === 1 && isObjectLike(value) && !isPlainObject(value); - } - - /** - * Checks if `value` is empty. A value is considered empty unless it's an - * `arguments` object, array, string, or jQuery-like collection with a length - * greater than `0` or an object with own enumerable properties. - * - * @static - * @memberOf _ - * @category Lang - * @param {Array|Object|string} value The value to inspect. - * @returns {boolean} Returns `true` if `value` is empty, else `false`. - * @example - * - * _.isEmpty(null); - * // => true - * - * _.isEmpty(true); - * // => true - * - * _.isEmpty(1); - * // => true - * - * _.isEmpty([1, 2, 3]); - * // => false - * - * _.isEmpty({ 'a': 1 }); - * // => false - */ - function isEmpty(value) { - return (!isObjectLike(value) || isFunction(value.splice)) - ? !size(value) - : !keys(value).length; - } - - /** - * Performs a deep comparison between two values to determine if they are - * equivalent. - * - * **Note:** This method supports comparing arrays, array buffers, booleans, - * date objects, error objects, maps, numbers, `Object` objects, regexes, - * sets, strings, symbols, and typed arrays. `Object` objects are compared - * by their own, not inherited, enumerable properties. Functions and DOM - * nodes are **not** supported. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var object = { 'user': 'fred' }; - * var other = { 'user': 'fred' }; - * - * _.isEqual(object, other); - * // => true - * - * object === other; - * // => false - */ - function isEqual(value, other) { - return baseIsEqual(value, other); - } - - /** - * This method is like `_.isEqual` except that it accepts `customizer` which is - * invoked to compare values. If `customizer` returns `undefined` comparisons are - * handled by the method instead. The `customizer` is invoked with up to seven arguments: - * (objValue, othValue [, index|key, object, other, stack]). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @param {Function} [customizer] The function to customize comparisons. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * function isGreeting(value) { - * return /^h(?:i|ello)$/.test(value); - * } - * - * function customizer(objValue, othValue) { - * if (isGreeting(objValue) && isGreeting(othValue)) { - * return true; - * } - * } - * - * var array = ['hello', 'goodbye']; - * var other = ['hi', 'goodbye']; - * - * _.isEqualWith(array, other, customizer); - * // => true - */ - function isEqualWith(value, other, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - var result = customizer ? customizer(value, other) : undefined; - return result === undefined ? baseIsEqual(value, other, customizer) : !!result; - } - - /** - * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`, - * `SyntaxError`, `TypeError`, or `URIError` object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an error object, else `false`. - * @example - * - * _.isError(new Error); - * // => true - * - * _.isError(Error); - * // => false - */ - function isError(value) { - return isObjectLike(value) && - typeof value.message == 'string' && objectToString.call(value) == errorTag; - } - - /** - * Checks if `value` is a finite primitive number. - * - * **Note:** This method is based on [`Number.isFinite`](https://mdn.io/Number/isFinite). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. - * @example - * - * _.isFinite(3); - * // => true - * - * _.isFinite(Number.MAX_VALUE); - * // => true - * - * _.isFinite(3.14); - * // => true - * - * _.isFinite(Infinity); - * // => false - */ - function isFinite(value) { - return typeof value == 'number' && nativeIsFinite(value); - } - - /** - * Checks if `value` is classified as a `Function` object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isFunction(_); - * // => true - * - * _.isFunction(/abc/); - * // => false - */ - function isFunction(value) { - // The use of `Object#toString` avoids issues with the `typeof` operator - // in Safari 8 which returns 'object' for typed array constructors, and - // PhantomJS 1.9 which returns 'function' for `NodeList` instances. - var tag = isObject(value) ? objectToString.call(value) : ''; - return tag == funcTag || tag == genTag; - } - - /** - * Checks if `value` is an integer. - * - * **Note:** This method is based on [`Number.isInteger`](https://mdn.io/Number/isInteger). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an integer, else `false`. - * @example - * - * _.isInteger(3); - * // => true - * - * _.isInteger(Number.MIN_VALUE); - * // => false - * - * _.isInteger(Infinity); - * // => false - * - * _.isInteger('3'); - * // => false - */ - function isInteger(value) { - return typeof value == 'number' && value == toInteger(value); - } - - /** - * Checks if `value` is a valid array-like length. - * - * **Note:** This function is loosely based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. - * @example - * - * _.isLength(3); - * // => true - * - * _.isLength(Number.MIN_VALUE); - * // => false - * - * _.isLength(Infinity); - * // => false - * - * _.isLength('3'); - * // => false - */ - function isLength(value) { - return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; - } - - /** - * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. - * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(_.noop); - * // => true - * - * _.isObject(null); - * // => false - */ - function isObject(value) { - // Avoid a V8 JIT bug in Chrome 19-20. - // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. - var type = typeof value; - return !!value && (type == 'object' || type == 'function'); - } - - /** - * Checks if `value` is object-like. A value is object-like if it's not `null` - * and has a `typeof` result of "object". - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is object-like, else `false`. - * @example - * - * _.isObjectLike({}); - * // => true - * - * _.isObjectLike([1, 2, 3]); - * // => true - * - * _.isObjectLike(_.noop); - * // => false - * - * _.isObjectLike(null); - * // => false - */ - function isObjectLike(value) { - return !!value && typeof value == 'object'; - } - - /** - * Performs a deep comparison between `object` and `source` to determine if - * `object` contains equivalent property values. - * - * **Note:** This method supports comparing the same values as `_.isEqual`. - * - * @static - * @memberOf _ - * @category Lang - * @param {Object} object The object to inspect. - * @param {Object} source The object of property values to match. - * @returns {boolean} Returns `true` if `object` is a match, else `false`. - * @example - * - * var object = { 'user': 'fred', 'age': 40 }; - * - * _.isMatch(object, { 'age': 40 }); - * // => true - * - * _.isMatch(object, { 'age': 36 }); - * // => false - */ - function isMatch(object, source) { - return object === source || baseIsMatch(object, source, getMatchData(source)); - } - - /** - * This method is like `_.isMatch` except that it accepts `customizer` which - * is invoked to compare values. If `customizer` returns `undefined` comparisons - * are handled by the method instead. The `customizer` is invoked with three - * arguments: (objValue, srcValue, index|key, object, source). - * - * @static - * @memberOf _ - * @category Lang - * @param {Object} object The object to inspect. - * @param {Object} source The object of property values to match. - * @param {Function} [customizer] The function to customize comparisons. - * @returns {boolean} Returns `true` if `object` is a match, else `false`. - * @example - * - * function isGreeting(value) { - * return /^h(?:i|ello)$/.test(value); - * } - * - * function customizer(objValue, srcValue) { - * if (isGreeting(objValue) && isGreeting(srcValue)) { - * return true; - * } - * } - * - * var object = { 'greeting': 'hello' }; - * var source = { 'greeting': 'hi' }; - * - * _.isMatchWith(object, source, customizer); - * // => true - */ - function isMatchWith(object, source, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return baseIsMatch(object, source, getMatchData(source), customizer); - } - - /** - * Checks if `value` is `NaN`. - * - * **Note:** This method is not the same as [`isNaN`](https://es5.github.io/#x15.1.2.4) - * which returns `true` for `undefined` and other non-numeric values. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. - * @example - * - * _.isNaN(NaN); - * // => true - * - * _.isNaN(new Number(NaN)); - * // => true - * - * isNaN(undefined); - * // => true - * - * _.isNaN(undefined); - * // => false - */ - function isNaN(value) { - // An `NaN` primitive is the only value that is not equal to itself. - // Perform the `toStringTag` check first to avoid errors with some ActiveX objects in IE. - return isNumber(value) && value != +value; - } - - /** - * Checks if `value` is a native function. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a native function, else `false`. - * @example - * - * _.isNative(Array.prototype.push); - * // => true - * - * _.isNative(_); - * // => false - */ - function isNative(value) { - if (value == null) { - return false; - } - if (isFunction(value)) { - return reIsNative.test(funcToString.call(value)); - } - return isObjectLike(value) && - (isHostObject(value) ? reIsNative : reIsHostCtor).test(value); - } - - /** - * Checks if `value` is `null`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `null`, else `false`. - * @example - * - * _.isNull(null); - * // => true - * - * _.isNull(void 0); - * // => false - */ - function isNull(value) { - return value === null; - } - - /** - * Checks if `value` is `null` or `undefined`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is nullish, else `false`. - * @example - * - * _.isNil(null); - * // => true - * - * _.isNil(void 0); - * // => true - * - * _.isNil(NaN); - * // => false - */ - function isNil(value) { - return value == null; - } - - /** - * Checks if `value` is classified as a `Number` primitive or object. - * - * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are classified - * as numbers, use the `_.isFinite` method. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isNumber(3); - * // => true - * - * _.isNumber(Number.MIN_VALUE); - * // => true - * - * _.isNumber(Infinity); - * // => true - * - * _.isNumber('3'); - * // => false - */ - function isNumber(value) { - return typeof value == 'number' || - (isObjectLike(value) && objectToString.call(value) == numberTag); - } - - /** - * Checks if `value` is a plain object, that is, an object created by the - * `Object` constructor or one with a `[[Prototype]]` of `null`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. - * @example - * - * function Foo() { - * this.a = 1; - * } - * - * _.isPlainObject(new Foo); - * // => false - * - * _.isPlainObject([1, 2, 3]); - * // => false - * - * _.isPlainObject({ 'x': 0, 'y': 0 }); - * // => true - * - * _.isPlainObject(Object.create(null)); - * // => true - */ - function isPlainObject(value) { - if (!isObjectLike(value) || objectToString.call(value) != objectTag || isHostObject(value)) { - return false; - } - var proto = objectProto; - if (typeof value.constructor == 'function') { - proto = getPrototypeOf(value); - } - if (proto === null) { - return true; - } - var Ctor = proto.constructor; - return (typeof Ctor == 'function' && - Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString); - } - - /** - * Checks if `value` is classified as a `RegExp` object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isRegExp(/abc/); - * // => true - * - * _.isRegExp('/abc/'); - * // => false - */ - function isRegExp(value) { - return isObject(value) && objectToString.call(value) == regexpTag; - } - - /** - * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754 - * double precision number which isn't the result of a rounded unsafe integer. - * - * **Note:** This method is based on [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`. - * @example - * - * _.isSafeInteger(3); - * // => true - * - * _.isSafeInteger(Number.MIN_VALUE); - * // => false - * - * _.isSafeInteger(Infinity); - * // => false - * - * _.isSafeInteger('3'); - * // => false - */ - function isSafeInteger(value) { - return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER; - } - - /** - * Checks if `value` is classified as a `String` primitive or object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isString('abc'); - * // => true - * - * _.isString(1); - * // => false - */ - function isString(value) { - return typeof value == 'string' || - (!isArray(value) && isObjectLike(value) && objectToString.call(value) == stringTag); - } - - /** - * Checks if `value` is classified as a `Symbol` primitive or object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isSymbol(Symbol.iterator); - * // => true - * - * _.isSymbol('abc'); - * // => false - */ - function isSymbol(value) { - return typeof value == 'symbol' || - (isObjectLike(value) && objectToString.call(value) == symbolTag); - } - - /** - * Checks if `value` is classified as a typed array. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. - * @example - * - * _.isTypedArray(new Uint8Array); - * // => true - * - * _.isTypedArray([]); - * // => false - */ - function isTypedArray(value) { - return isObjectLike(value) && isLength(value.length) && !!typedArrayTags[objectToString.call(value)]; - } - - /** - * Checks if `value` is `undefined`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. - * @example - * - * _.isUndefined(void 0); - * // => true - * - * _.isUndefined(null); - * // => false - */ - function isUndefined(value) { - return value === undefined; - } - - /** - * Checks if `value` is less than `other`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is less than `other`, else `false`. - * @example - * - * _.lt(1, 3); - * // => true - * - * _.lt(3, 3); - * // => false - * - * _.lt(3, 1); - * // => false - */ - function lt(value, other) { - return value < other; - } - - /** - * Checks if `value` is less than or equal to `other`. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is less than or equal to `other`, else `false`. - * @example - * - * _.lte(1, 3); - * // => true - * - * _.lte(3, 3); - * // => true - * - * _.lte(3, 1); - * // => false - */ - function lte(value, other) { - return value <= other; - } - - /** - * Converts `value` to an array. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to convert. - * @returns {Array} Returns the converted array. - * @example - * - * _.toArray({ 'a': 1, 'b': 2 }); - * // => [1, 2] - * - * _.toArray('abc'); - * // => ['a', 'b', 'c'] - * - * _.toArray(1); - * // => [] - * - * _.toArray(null); - * // => [] - */ - function toArray(value) { - if (!value) { - return []; - } - if (isArrayLike(value)) { - return isString(value) ? stringToArray(value) : copyArray(value); - } - if (iteratorSymbol && value[iteratorSymbol]) { - return iteratorToArray(value[iteratorSymbol]()); - } - var tag = getTag(value), - func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); - - return func(value); - } - - /** - * Converts `value` to an integer. - * - * **Note:** This function is loosely based on [`ToInteger`](http://www.ecma-international.org/ecma-262/6.0/#sec-tointeger). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to convert. - * @returns {number} Returns the converted integer. - * @example - * - * _.toInteger(3); - * // => 3 - * - * _.toInteger(Number.MIN_VALUE); - * // => 0 - * - * _.toInteger(Infinity); - * // => 1.7976931348623157e+308 - * - * _.toInteger('3'); - * // => 3 - */ - function toInteger(value) { - if (!value) { - return value === 0 ? value : 0; - } - value = toNumber(value); - if (value === INFINITY || value === -INFINITY) { - var sign = (value < 0 ? -1 : 1); - return sign * MAX_INTEGER; - } - var remainder = value % 1; - return value === value ? (remainder ? value - remainder : value) : 0; - } - - /** - * Converts `value` to an integer suitable for use as the length of an - * array-like object. - * - * **Note:** This method is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength). - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to convert. - * @return {number} Returns the converted integer. - * @example - * - * _.toLength(3); - * // => 3 - * - * _.toLength(Number.MIN_VALUE); - * // => 0 - * - * _.toLength(Infinity); - * // => 4294967295 - * - * _.toLength('3'); - * // => 3 - */ - function toLength(value) { - return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; - } - - /** - * Converts `value` to a number. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to process. - * @returns {number} Returns the number. - * @example - * - * _.toNumber(3); - * // => 3 - * - * _.toNumber(Number.MIN_VALUE); - * // => 5e-324 - * - * _.toNumber(Infinity); - * // => Infinity - * - * _.toNumber('3'); - * // => 3 - */ - function toNumber(value) { - if (isObject(value)) { - var other = isFunction(value.valueOf) ? value.valueOf() : value; - value = isObject(other) ? (other + '') : other; - } - if (typeof value != 'string') { - return value === 0 ? value : +value; - } - value = value.replace(reTrim, ''); - var isBinary = reIsBinary.test(value); - return (isBinary || reIsOctal.test(value)) - ? freeParseInt(value.slice(2), isBinary ? 2 : 8) - : (reIsBadHex.test(value) ? NAN : +value); - } - - /** - * Converts `value` to a plain object flattening inherited enumerable - * properties of `value` to own properties of the plain object. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to convert. - * @returns {Object} Returns the converted plain object. - * @example - * - * function Foo() { - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.assign({ 'a': 1 }, new Foo); - * // => { 'a': 1, 'b': 2 } - * - * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); - * // => { 'a': 1, 'b': 2, 'c': 3 } - */ - function toPlainObject(value) { - return copyObject(value, keysIn(value)); - } - - /** - * Converts `value` to a safe integer. A safe integer can be compared and - * represented correctly. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to convert. - * @returns {number} Returns the converted integer. - * @example - * - * _.toSafeInteger(3); - * // => 3 - * - * _.toSafeInteger(Number.MIN_VALUE); - * // => 0 - * - * _.toSafeInteger(Infinity); - * // => 9007199254740991 - * - * _.toSafeInteger('3'); - * // => 3 - */ - function toSafeInteger(value) { - return baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER); - } - - /** - * Converts `value` to a string if it's not one. An empty string is returned - * for `null` and `undefined` values. The sign of `-0` is preserved. - * - * @static - * @memberOf _ - * @category Lang - * @param {*} value The value to process. - * @returns {string} Returns the string. - * @example - * - * _.toString(null); - * // => '' - * - * _.toString(-0); - * // => '-0' - * - * _.toString([1, 2, 3]); - * // => '1,2,3' - */ - function toString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (value == null) { - return ''; - } - if (isSymbol(value)) { - return _Symbol ? symbolToString.call(value) : ''; - } - var result = (value + ''); - return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; - } - - /*------------------------------------------------------------------------*/ - - /** - * Assigns own enumerable properties of source objects to the destination - * object. Source objects are applied from left to right. Subsequent sources - * overwrite property assignments of previous sources. - * - * **Note:** This method mutates `object` and is loosely based on - * [`Object.assign`](https://mdn.io/Object/assign). - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @example - * - * function Foo() { - * this.c = 3; - * } - * - * function Bar() { - * this.e = 5; - * } - * - * Foo.prototype.d = 4; - * Bar.prototype.f = 6; - * - * _.assign({ 'a': 1 }, new Foo, new Bar); - * // => { 'a': 1, 'c': 3, 'e': 5 } - */ - var assign = createAssigner(function(object, source) { - copyObject(source, keys(source), object); - }); - - /** - * This method is like `_.assign` except that it iterates over own and - * inherited source properties. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @alias extend - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @example - * - * function Foo() { - * this.b = 2; - * } - * - * function Bar() { - * this.d = 4; - * } - * - * Foo.prototype.c = 3; - * Bar.prototype.e = 5; - * - * _.assignIn({ 'a': 1 }, new Foo, new Bar); - * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5 } - */ - var assignIn = createAssigner(function(object, source) { - copyObject(source, keysIn(source), object); - }); - - /** - * This method is like `_.assignIn` except that it accepts `customizer` which - * is invoked to produce the assigned values. If `customizer` returns `undefined` - * assignment is handled by the method instead. The `customizer` is invoked - * with five arguments: (objValue, srcValue, key, object, source). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @alias extendWith - * @category Object - * @param {Object} object The destination object. - * @param {...Object} sources The source objects. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * function customizer(objValue, srcValue) { - * return _.isUndefined(objValue) ? srcValue : objValue; - * } - * - * var defaults = _.partialRight(_.assignInWith, customizer); - * - * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); - * // => { 'a': 1, 'b': 2 } - */ - var assignInWith = createAssigner(function(object, source, customizer) { - copyObjectWith(source, keysIn(source), object, customizer); - }); - - /** - * This method is like `_.assign` except that it accepts `customizer` which - * is invoked to produce the assigned values. If `customizer` returns `undefined` - * assignment is handled by the method instead. The `customizer` is invoked - * with five arguments: (objValue, srcValue, key, object, source). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} sources The source objects. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * function customizer(objValue, srcValue) { - * return _.isUndefined(objValue) ? srcValue : objValue; - * } - * - * var defaults = _.partialRight(_.assignWith, customizer); - * - * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); - * // => { 'a': 1, 'b': 2 } - */ - var assignWith = createAssigner(function(object, source, customizer) { - copyObjectWith(source, keys(source), object, customizer); - }); - - /** - * Creates an array of values corresponding to `paths` of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {...(string|string[])} [paths] The property paths of elements to pick, - * specified individually or in arrays. - * @returns {Array} Returns the new array of picked elements. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; - * - * _.at(object, ['a[0].b.c', 'a[1]']); - * // => [3, 4] - * - * _.at(['a', 'b', 'c'], 0, 2); - * // => ['a', 'c'] - */ - var at = rest(function(object, paths) { - return baseAt(object, baseFlatten(paths)); - }); - - /** - * Creates an object that inherits from the `prototype` object. If a `properties` - * object is provided its own enumerable properties are assigned to the created object. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} prototype The object to inherit from. - * @param {Object} [properties] The properties to assign to the object. - * @returns {Object} Returns the new object. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * function Circle() { - * Shape.call(this); - * } - * - * Circle.prototype = _.create(Shape.prototype, { - * 'constructor': Circle - * }); - * - * var circle = new Circle; - * circle instanceof Circle; - * // => true - * - * circle instanceof Shape; - * // => true - */ - function create(prototype, properties) { - var result = baseCreate(prototype); - return properties ? baseAssign(result, properties) : result; - } - - /** - * Assigns own and inherited enumerable properties of source objects to the - * destination object for all destination properties that resolve to `undefined`. - * Source objects are applied from left to right. Once a property is set, - * additional values of the same property are ignored. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @example - * - * _.defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); - * // => { 'user': 'barney', 'age': 36 } - */ - var defaults = rest(function(args) { - args.push(undefined, assignInDefaults); - return apply(assignInWith, undefined, args); - }); - - /** - * This method is like `_.defaults` except that it recursively assigns - * default properties. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @example - * - * _.defaultsDeep({ 'user': { 'name': 'barney' } }, { 'user': { 'name': 'fred', 'age': 36 } }); - * // => { 'user': { 'name': 'barney', 'age': 36 } } - * - */ - var defaultsDeep = rest(function(args) { - args.push(undefined, mergeDefaults); - return apply(mergeWith, undefined, args); - }); - - /** - * This method is like `_.find` except that it returns the key of the first - * element `predicate` returns truthy for instead of the element itself. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to search. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {string|undefined} Returns the key of the matched element, else `undefined`. - * @example - * - * var users = { - * 'barney': { 'age': 36, 'active': true }, - * 'fred': { 'age': 40, 'active': false }, - * 'pebbles': { 'age': 1, 'active': true } - * }; - * - * _.findKey(users, function(o) { return o.age < 40; }); - * // => 'barney' (iteration order is not guaranteed) - * - * // using the `_.matches` iteratee shorthand - * _.findKey(users, { 'age': 1, 'active': true }); - * // => 'pebbles' - * - * // using the `_.matchesProperty` iteratee shorthand - * _.findKey(users, ['active', false]); - * // => 'fred' - * - * // using the `_.property` iteratee shorthand - * _.findKey(users, 'active'); - * // => 'barney' - */ - function findKey(object, predicate) { - return baseFind(object, getIteratee(predicate, 3), baseForOwn, true); - } - - /** - * This method is like `_.findKey` except that it iterates over elements of - * a collection in the opposite order. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to search. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per iteration. - * @returns {string|undefined} Returns the key of the matched element, else `undefined`. - * @example - * - * var users = { - * 'barney': { 'age': 36, 'active': true }, - * 'fred': { 'age': 40, 'active': false }, - * 'pebbles': { 'age': 1, 'active': true } - * }; - * - * _.findLastKey(users, function(o) { return o.age < 40; }); - * // => returns 'pebbles' assuming `_.findKey` returns 'barney' - * - * // using the `_.matches` iteratee shorthand - * _.findLastKey(users, { 'age': 36, 'active': true }); - * // => 'barney' - * - * // using the `_.matchesProperty` iteratee shorthand - * _.findLastKey(users, ['active', false]); - * // => 'fred' - * - * // using the `_.property` iteratee shorthand - * _.findLastKey(users, 'active'); - * // => 'pebbles' - */ - function findLastKey(object, predicate) { - return baseFind(object, getIteratee(predicate, 3), baseForOwnRight, true); - } - - /** - * Iterates over own and inherited enumerable properties of an object invoking - * `iteratee` for each property. The iteratee is invoked with three arguments: - * (value, key, object). Iteratee functions may exit iteration early by explicitly - * returning `false`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forIn(new Foo, function(value, key) { - * console.log(key); - * }); - * // => logs 'a', 'b', then 'c' (iteration order is not guaranteed) - */ - function forIn(object, iteratee) { - return object == null ? object : baseFor(object, toFunction(iteratee), keysIn); - } - - /** - * This method is like `_.forIn` except that it iterates over properties of - * `object` in the opposite order. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forInRight(new Foo, function(value, key) { - * console.log(key); - * }); - * // => logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c' - */ - function forInRight(object, iteratee) { - return object == null ? object : baseForRight(object, toFunction(iteratee), keysIn); - } - - /** - * Iterates over own enumerable properties of an object invoking `iteratee` - * for each property. The iteratee is invoked with three arguments: - * (value, key, object). Iteratee functions may exit iteration early by - * explicitly returning `false`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forOwn(new Foo, function(value, key) { - * console.log(key); - * }); - * // => logs 'a' then 'b' (iteration order is not guaranteed) - */ - function forOwn(object, iteratee) { - return object && baseForOwn(object, toFunction(iteratee)); - } - - /** - * This method is like `_.forOwn` except that it iterates over properties of - * `object` in the opposite order. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forOwnRight(new Foo, function(value, key) { - * console.log(key); - * }); - * // => logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b' - */ - function forOwnRight(object, iteratee) { - return object && baseForOwnRight(object, toFunction(iteratee)); - } - - /** - * Creates an array of function property names from own enumerable properties - * of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to inspect. - * @returns {Array} Returns the new array of property names. - * @example - * - * function Foo() { - * this.a = _.constant('a'); - * this.b = _.constant('b'); - * } - * - * Foo.prototype.c = _.constant('c'); - * - * _.functions(new Foo); - * // => ['a', 'b'] - */ - function functions(object) { - return object == null ? [] : baseFunctions(object, keys(object)); - } - - /** - * Creates an array of function property names from own and inherited - * enumerable properties of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to inspect. - * @returns {Array} Returns the new array of property names. - * @example - * - * function Foo() { - * this.a = _.constant('a'); - * this.b = _.constant('b'); - * } - * - * Foo.prototype.c = _.constant('c'); - * - * _.functionsIn(new Foo); - * // => ['a', 'b', 'c'] - */ - function functionsIn(object) { - return object == null ? [] : baseFunctions(object, keysIn(object)); - } - - /** - * Gets the value at `path` of `object`. If the resolved value is - * `undefined` the `defaultValue` is used in its place. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to get. - * @param {*} [defaultValue] The value returned if the resolved value is `undefined`. - * @returns {*} Returns the resolved value. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }] }; - * - * _.get(object, 'a[0].b.c'); - * // => 3 - * - * _.get(object, ['a', '0', 'b', 'c']); - * // => 3 - * - * _.get(object, 'a.b.c', 'default'); - * // => 'default' - */ - function get(object, path, defaultValue) { - var result = object == null ? undefined : baseGet(object, path); - return result === undefined ? defaultValue : result; - } - - /** - * Checks if `path` is a direct property of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path to check. - * @returns {boolean} Returns `true` if `path` exists, else `false`. - * @example - * - * var object = { 'a': { 'b': { 'c': 3 } } }; - * var other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) }); - * - * _.has(object, 'a'); - * // => true - * - * _.has(object, 'a.b.c'); - * // => true - * - * _.has(object, ['a', 'b', 'c']); - * // => true - * - * _.has(other, 'a'); - * // => false - */ - function has(object, path) { - return hasPath(object, path, baseHas); - } - - /** - * Checks if `path` is a direct or inherited property of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path to check. - * @returns {boolean} Returns `true` if `path` exists, else `false`. - * @example - * - * var object = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) }); - * - * _.hasIn(object, 'a'); - * // => true - * - * _.hasIn(object, 'a.b.c'); - * // => true - * - * _.hasIn(object, ['a', 'b', 'c']); - * // => true - * - * _.hasIn(object, 'b'); - * // => false - */ - function hasIn(object, path) { - return hasPath(object, path, baseHasIn); - } - - /** - * Creates an object composed of the inverted keys and values of `object`. - * If `object` contains duplicate values, subsequent values overwrite property - * assignments of previous values unless `multiVal` is `true`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to invert. - * @param {boolean} [multiVal] Allow multiple values per key. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Object} Returns the new inverted object. - * @example - * - * var object = { 'a': 1, 'b': 2, 'c': 1 }; - * - * _.invert(object); - * // => { '1': 'c', '2': 'b' } - * - * // with `multiVal` - * _.invert(object, true); - * // => { '1': ['a', 'c'], '2': ['b'] } - */ - function invert(object, multiVal, guard) { - return arrayReduce(keys(object), function(result, key) { - var value = object[key]; - if (multiVal && !guard) { - if (hasOwnProperty.call(result, value)) { - result[value].push(key); - } else { - result[value] = [key]; - } - } - else { - result[value] = key; - } - return result; - }, {}); - } - - /** - * Invokes the method at `path` of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the method to invoke. - * @param {...*} [args] The arguments to invoke the method with. - * @returns {*} Returns the result of the invoked method. - * @example - * - * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] }; - * - * _.invoke(object, 'a[0].b.c.slice', 1, 3); - * // => [2, 3] - */ - var invoke = rest(baseInvoke); - - /** - * Creates an array of the own enumerable property names of `object`. - * - * **Note:** Non-object values are coerced to objects. See the - * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys) - * for more details. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.keys(new Foo); - * // => ['a', 'b'] (iteration order is not guaranteed) - * - * _.keys('hi'); - * // => ['0', '1'] - */ - function keys(object) { - var isProto = isPrototype(object); - if (!(isProto || isArrayLike(object))) { - return baseKeys(object); - } - var indexes = indexKeys(object), - skipIndexes = !!indexes, - result = indexes || [], - length = result.length; - - for (var key in object) { - if (baseHas(object, key) && - !(skipIndexes && (key == 'length' || isIndex(key, length))) && - !(isProto && key == 'constructor')) { - result.push(key); - } - } - return result; - } - - /** - * Creates an array of the own and inherited enumerable property names of `object`. - * - * **Note:** Non-object values are coerced to objects. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.keysIn(new Foo); - * // => ['a', 'b', 'c'] (iteration order is not guaranteed) - */ - function keysIn(object) { - var index = -1, - isProto = isPrototype(object), - props = baseKeysIn(object), - propsLength = props.length, - indexes = indexKeys(object), - skipIndexes = !!indexes, - result = indexes || [], - length = result.length; - - while (++index < propsLength) { - var key = props[index]; - if (!(skipIndexes && (key == 'length' || isIndex(key, length))) && - !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { - result.push(key); - } - } - return result; - } - - /** - * The opposite of `_.mapValues`; this method creates an object with the - * same values as `object` and keys generated by running each own enumerable - * property of `object` through `iteratee`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns the new mapped object. - * @example - * - * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { - * return key + value; - * }); - * // => { 'a1': 1, 'b2': 2 } - */ - function mapKeys(object, iteratee) { - var result = {}; - iteratee = getIteratee(iteratee, 3); - - baseForOwn(object, function(value, key, object) { - result[iteratee(value, key, object)] = value; - }); - return result; - } - - /** - * Creates an object with the same keys as `object` and values generated by - * running each own enumerable property of `object` through `iteratee`. The - * iteratee function is invoked with three arguments: (value, key, object). - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns the new mapped object. - * @example - * - * var users = { - * 'fred': { 'user': 'fred', 'age': 40 }, - * 'pebbles': { 'user': 'pebbles', 'age': 1 } - * }; - * - * _.mapValues(users, function(o) { return o.age; }); - * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) - * - * // using the `_.property` iteratee shorthand - * _.mapValues(users, 'age'); - * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) - */ - function mapValues(object, iteratee) { - var result = {}; - iteratee = getIteratee(iteratee, 3); - - baseForOwn(object, function(value, key, object) { - result[key] = iteratee(value, key, object); - }); - return result; - } - - /** - * Recursively merges own and inherited enumerable properties of source - * objects into the destination object, skipping source properties that resolve - * to `undefined`. Array and plain object properties are merged recursively. - * Other objects and value types are overridden by assignment. Source objects - * are applied from left to right. Subsequent sources overwrite property - * assignments of previous sources. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @example - * - * var users = { - * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }] - * }; - * - * var ages = { - * 'data': [{ 'age': 36 }, { 'age': 40 }] - * }; - * - * _.merge(users, ages); - * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] } - */ - var merge = createAssigner(function(object, source) { - baseMerge(object, source); - }); - - /** - * This method is like `_.merge` except that it accepts `customizer` which - * is invoked to produce the merged values of the destination and source - * properties. If `customizer` returns `undefined` merging is handled by the - * method instead. The `customizer` is invoked with seven arguments: - * (objValue, srcValue, key, object, source, stack). - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} sources The source objects. - * @param {Function} customizer The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * function customizer(objValue, srcValue) { - * if (_.isArray(objValue)) { - * return objValue.concat(srcValue); - * } - * } - * - * var object = { - * 'fruits': ['apple'], - * 'vegetables': ['beet'] - * }; - * - * var other = { - * 'fruits': ['banana'], - * 'vegetables': ['carrot'] - * }; - * - * _.mergeWith(object, other, customizer); - * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } - */ - var mergeWith = createAssigner(function(object, source, customizer) { - baseMerge(object, source, customizer); - }); - - /** - * The opposite of `_.pick`; this method creates an object composed of the - * own and inherited enumerable properties of `object` that are not omitted. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The source object. - * @param {...(string|string[])} [props] The property names to omit, specified - * individually or in arrays.. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.omit(object, ['a', 'c']); - * // => { 'b': '2' } - */ - var omit = rest(function(object, props) { - if (object == null) { - return {}; - } - props = arrayMap(baseFlatten(props), String); - return basePick(object, baseDifference(keysIn(object), props)); - }); - - /** - * The opposite of `_.pickBy`; this method creates an object composed of the - * own and inherited enumerable properties of `object` that `predicate` - * doesn't return truthy for. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The source object. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per property. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.omitBy(object, _.isNumber); - * // => { 'b': '2' } - */ - function omitBy(object, predicate) { - predicate = getIteratee(predicate); - return basePickBy(object, function(value) { - return !predicate(value); - }); - } - - /** - * Creates an object composed of the picked `object` properties. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The source object. - * @param {...(string|string[])} [props] The property names to pick, specified - * individually or in arrays. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.pick(object, ['a', 'c']); - * // => { 'a': 1, 'c': 3 } - */ - var pick = rest(function(object, props) { - return object == null ? {} : basePick(object, baseFlatten(props)); - }); - - /** - * Creates an object composed of the `object` properties `predicate` returns - * truthy for. The predicate is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The source object. - * @param {Function|Object|string} [predicate=_.identity] The function invoked per property. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.pickBy(object, _.isNumber); - * // => { 'a': 1, 'c': 3 } - */ - function pickBy(object, predicate) { - return object == null ? {} : basePickBy(object, getIteratee(predicate)); - } - - /** - * This method is like `_.get` except that if the resolved value is a function - * it's invoked with the `this` binding of its parent object and its result - * is returned. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to resolve. - * @param {*} [defaultValue] The value returned if the resolved value is `undefined`. - * @returns {*} Returns the resolved value. - * @example - * - * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] }; - * - * _.result(object, 'a[0].b.c1'); - * // => 3 - * - * _.result(object, 'a[0].b.c2'); - * // => 4 - * - * _.result(object, 'a[0].b.c3', 'default'); - * // => 'default' - * - * _.result(object, 'a[0].b.c3', _.constant('default')); - * // => 'default' - */ - function result(object, path, defaultValue) { - if (!isKey(path, object)) { - path = baseToPath(path); - var result = get(object, path); - object = parent(object, path); - } else { - result = object == null ? undefined : object[path]; - } - if (result === undefined) { - result = defaultValue; - } - return isFunction(result) ? result.call(object) : result; - } - - /** - * Sets the value at `path` of `object`. If a portion of `path` doesn't exist - * it's created. Arrays are created for missing index properties while objects - * are created for all other missing properties. Use `_.setWith` to customize - * `path` creation. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {*} value The value to set. - * @returns {Object} Returns `object`. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }] }; - * - * _.set(object, 'a[0].b.c', 4); - * console.log(object.a[0].b.c); - * // => 4 - * - * _.set(object, 'x[0].y.z', 5); - * console.log(object.x[0].y.z); - * // => 5 - */ - function set(object, path, value) { - return object == null ? object : baseSet(object, path, value); - } - - /** - * This method is like `_.set` except that it accepts `customizer` which is - * invoked to produce the objects of `path`. If `customizer` returns `undefined` - * path creation is handled by the method instead. The `customizer` is invoked - * with three arguments: (nsValue, key, nsObject). - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {*} value The value to set. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * _.setWith({ '0': { 'length': 2 } }, '[0][1][2]', 3, Object); - * // => { '0': { '1': { '2': 3 }, 'length': 2 } } - */ - function setWith(object, path, value, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return object == null ? object : baseSet(object, path, value, customizer); - } - - /** - * Creates an array of own enumerable key-value pairs for `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the new array of key-value pairs. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.toPairs(new Foo); - * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed) - */ - function toPairs(object) { - return baseToPairs(object, keys(object)); - } - - /** - * Creates an array of own and inherited enumerable key-value pairs for `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the new array of key-value pairs. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.toPairsIn(new Foo); - * // => [['a', 1], ['b', 2], ['c', 1]] (iteration order is not guaranteed) - */ - function toPairsIn(object) { - return baseToPairs(object, keysIn(object)); - } - - /** - * An alternative to `_.reduce`; this method transforms `object` to a new - * `accumulator` object which is the result of running each of its own enumerable - * properties through `iteratee`, with each invocation potentially mutating - * the `accumulator` object. The iteratee is invoked with four arguments: - * (accumulator, value, key, object). Iteratee functions may exit iteration - * early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @category Object - * @param {Array|Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {*} [accumulator] The custom accumulator value. - * @returns {*} Returns the accumulated value. - * @example - * - * _.transform([2, 3, 4], function(result, n) { - * result.push(n *= n); - * return n % 2 == 0; - * }); - * // => [4, 9] - * - * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { - * (result[value] || (result[value] = [])).push(key); - * }); - * // => { '1': ['a', 'c'], '2': ['b'] } - */ - function transform(object, iteratee, accumulator) { - var isArr = isArray(object) || isTypedArray(object); - iteratee = getIteratee(iteratee, 4); - - if (accumulator == null) { - if (isArr || isObject(object)) { - var Ctor = object.constructor; - if (isArr) { - accumulator = isArray(object) ? new Ctor : []; - } else { - accumulator = baseCreate(isFunction(Ctor) ? Ctor.prototype : undefined); - } - } else { - accumulator = {}; - } - } - (isArr ? arrayEach : baseForOwn)(object, function(value, index, object) { - return iteratee(accumulator, value, index, object); - }); - return accumulator; - } - - /** - * Removes the property at `path` of `object`. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to unset. - * @returns {boolean} Returns `true` if the property is deleted, else `false`. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 7 } }] }; - * _.unset(object, 'a[0].b.c'); - * // => true - * - * console.log(object); - * // => { 'a': [{ 'b': {} }] }; - * - * _.unset(object, 'a[0].b.c'); - * // => true - * - * console.log(object); - * // => { 'a': [{ 'b': {} }] }; - */ - function unset(object, path) { - return object == null ? true : baseUnset(object, path); - } - - /** - * Creates an array of the own enumerable property values of `object`. - * - * **Note:** Non-object values are coerced to objects. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property values. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.values(new Foo); - * // => [1, 2] (iteration order is not guaranteed) - * - * _.values('hi'); - * // => ['h', 'i'] - */ - function values(object) { - return object ? baseValues(object, keys(object)) : []; - } - - /** - * Creates an array of the own and inherited enumerable property values of `object`. - * - * **Note:** Non-object values are coerced to objects. - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property values. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.valuesIn(new Foo); - * // => [1, 2, 3] (iteration order is not guaranteed) - */ - function valuesIn(object) { - return object == null ? baseValues(object, keysIn(object)) : []; - } - - /*------------------------------------------------------------------------*/ - - /** - * Clamps `number` within the inclusive `lower` and `upper` bounds. - * - * @static - * @memberOf _ - * @category Number - * @param {number} number The number to clamp. - * @param {number} [lower] The lower bound. - * @param {number} upper The upper bound. - * @returns {number} Returns the clamped number. - * @example - * - * _.clamp(-10, -5, 5); - * // => -5 - * - * _.clamp(10, -5, 5); - * // => 5 - */ - function clamp(number, lower, upper) { - if (upper === undefined) { - upper = lower; - lower = undefined; - } - if (upper !== undefined) { - upper = toNumber(upper); - upper = upper === upper ? upper : 0; - } - if (lower !== undefined) { - lower = toNumber(lower); - lower = lower === lower ? lower : 0; - } - return baseClamp(toNumber(number), lower, upper); - } - - /** - * Checks if `n` is between `start` and up to but not including, `end`. If - * `end` is not specified it's set to `start` with `start` then set to `0`. - * If `start` is greater than `end` the params are swapped to support - * negative ranges. - * - * @static - * @memberOf _ - * @category Number - * @param {number} number The number to check. - * @param {number} [start=0] The start of the range. - * @param {number} end The end of the range. - * @returns {boolean} Returns `true` if `number` is in the range, else `false`. - * @example - * - * _.inRange(3, 2, 4); - * // => true - * - * _.inRange(4, 8); - * // => true - * - * _.inRange(4, 2); - * // => false - * - * _.inRange(2, 2); - * // => false - * - * _.inRange(1.2, 2); - * // => true - * - * _.inRange(5.2, 4); - * // => false - * - * _.inRange(-3, -2, -6); - * // => true - */ - function inRange(number, start, end) { - start = toNumber(start) || 0; - if (end === undefined) { - end = start; - start = 0; - } else { - end = toNumber(end) || 0; - } - number = toNumber(number); - return baseInRange(number, start, end); - } - - /** - * Produces a random number between the inclusive `lower` and `upper` bounds. - * If only one argument is provided a number between `0` and the given number - * is returned. If `floating` is `true`, or either `lower` or `upper` are floats, - * a floating-point number is returned instead of an integer. - * - * **Note:** JavaScript follows the IEEE-754 standard for resolving - * floating-point values which can produce unexpected results. - * - * @static - * @memberOf _ - * @category Number - * @param {number} [lower=0] The lower bound. - * @param {number} [upper=1] The upper bound. - * @param {boolean} [floating] Specify returning a floating-point number. - * @returns {number} Returns the random number. - * @example - * - * _.random(0, 5); - * // => an integer between 0 and 5 - * - * _.random(5); - * // => also an integer between 0 and 5 - * - * _.random(5, true); - * // => a floating-point number between 0 and 5 - * - * _.random(1.2, 5.2); - * // => a floating-point number between 1.2 and 5.2 - */ - function random(lower, upper, floating) { - if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) { - upper = floating = undefined; - } - if (floating === undefined) { - if (typeof upper == 'boolean') { - floating = upper; - upper = undefined; - } - else if (typeof lower == 'boolean') { - floating = lower; - lower = undefined; - } - } - if (lower === undefined && upper === undefined) { - lower = 0; - upper = 1; - } - else { - lower = toNumber(lower) || 0; - if (upper === undefined) { - upper = lower; - lower = 0; - } else { - upper = toNumber(upper) || 0; - } - } - if (lower > upper) { - var temp = lower; - lower = upper; - upper = temp; - } - if (floating || lower % 1 || upper % 1) { - var rand = nativeRandom(); - return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper); - } - return baseRandom(lower, upper); - } - - /*------------------------------------------------------------------------*/ - - /** - * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the camel cased string. - * @example - * - * _.camelCase('Foo Bar'); - * // => 'fooBar' - * - * _.camelCase('--foo-bar'); - * // => 'fooBar' - * - * _.camelCase('__foo_bar__'); - * // => 'fooBar' - */ - var camelCase = createCompounder(function(result, word, index) { - word = word.toLowerCase(); - return result + (index ? capitalize(word) : word); - }); - - /** - * Converts the first character of `string` to upper case and the remaining - * to lower case. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to capitalize. - * @returns {string} Returns the capitalized string. - * @example - * - * _.capitalize('FRED'); - * // => 'Fred' - */ - function capitalize(string) { - return upperFirst(toString(string).toLowerCase()); - } - - /** - * Deburrs `string` by converting [latin-1 supplementary letters](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) - * to basic latin letters and removing [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to deburr. - * @returns {string} Returns the deburred string. - * @example - * - * _.deburr('déjà vu'); - * // => 'deja vu' - */ - function deburr(string) { - string = toString(string); - return string && string.replace(reLatin1, deburrLetter).replace(reComboMark, ''); - } - - /** - * Checks if `string` ends with the given target string. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to search. - * @param {string} [target] The string to search for. - * @param {number} [position=string.length] The position to search from. - * @returns {boolean} Returns `true` if `string` ends with `target`, else `false`. - * @example - * - * _.endsWith('abc', 'c'); - * // => true - * - * _.endsWith('abc', 'b'); - * // => false - * - * _.endsWith('abc', 'b', 2); - * // => true - */ - function endsWith(string, target, position) { - string = toString(string); - target = typeof target == 'string' ? target : (target + ''); - - var length = string.length; - position = position === undefined - ? length - : baseClamp(toInteger(position), 0, length); - - position -= target.length; - return position >= 0 && string.indexOf(target, position) == position; - } - - /** - * Converts the characters "&", "<", ">", '"', "'", and "\`" in `string` to - * their corresponding HTML entities. - * - * **Note:** No other characters are escaped. To escape additional - * characters use a third-party library like [_he_](https://mths.be/he). - * - * Though the ">" character is escaped for symmetry, characters like - * ">" and "/" don't need escaping in HTML and have no special meaning - * unless they're part of a tag or unquoted attribute value. - * See [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) - * (under "semi-related fun fact") for more details. - * - * Backticks are escaped because in IE < 9, they can break out of - * attribute values or HTML comments. See [#59](https://html5sec.org/#59), - * [#102](https://html5sec.org/#102), [#108](https://html5sec.org/#108), and - * [#133](https://html5sec.org/#133) of the [HTML5 Security Cheatsheet](https://html5sec.org/) - * for more details. - * - * When working with HTML you should always [quote attribute values](http://wonko.com/post/html-escaping) - * to reduce XSS vectors. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to escape. - * @returns {string} Returns the escaped string. - * @example - * - * _.escape('fred, barney, & pebbles'); - * // => 'fred, barney, & pebbles' - */ - function escape(string) { - string = toString(string); - return (string && reHasUnescapedHtml.test(string)) - ? string.replace(reUnescapedHtml, escapeHtmlChar) - : string; - } - - /** - * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", - * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to escape. - * @returns {string} Returns the escaped string. - * @example - * - * _.escapeRegExp('[lodash](https://lodash.com/)'); - * // => '\[lodash\]\(https://lodash\.com/\)' - */ - function escapeRegExp(string) { - string = toString(string); - return (string && reHasRegExpChar.test(string)) - ? string.replace(reRegExpChar, '\\$&') - : string; - } - - /** - * Converts `string` to [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the kebab cased string. - * @example - * - * _.kebabCase('Foo Bar'); - * // => 'foo-bar' - * - * _.kebabCase('fooBar'); - * // => 'foo-bar' - * - * _.kebabCase('__foo_bar__'); - * // => 'foo-bar' - */ - var kebabCase = createCompounder(function(result, word, index) { - return result + (index ? '-' : '') + word.toLowerCase(); - }); - - /** - * Converts `string`, as space separated words, to lower case. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the lower cased string. - * @example - * - * _.lowerCase('--Foo-Bar'); - * // => 'foo bar' - * - * _.lowerCase('fooBar'); - * // => 'foo bar' - * - * _.lowerCase('__FOO_BAR__'); - * // => 'foo bar' - */ - var lowerCase = createCompounder(function(result, word, index) { - return result + (index ? ' ' : '') + word.toLowerCase(); - }); - - /** - * Converts the first character of `string` to lower case. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the converted string. - * @example - * - * _.lowerFirst('Fred'); - * // => 'fred' - * - * _.lowerFirst('FRED'); - * // => 'fRED' - */ - var lowerFirst = createCaseFirst('toLowerCase'); - - /** - * Converts the first character of `string` to upper case. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the converted string. - * @example - * - * _.upperFirst('fred'); - * // => 'Fred' - * - * _.upperFirst('FRED'); - * // => 'FRED' - */ - var upperFirst = createCaseFirst('toUpperCase'); - - /** - * Pads `string` on the left and right sides if it's shorter than `length`. - * Padding characters are truncated if they can't be evenly divided by `length`. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to pad. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padded string. - * @example - * - * _.pad('abc', 8); - * // => ' abc ' - * - * _.pad('abc', 8, '_-'); - * // => '_-abc_-_' - * - * _.pad('abc', 3); - * // => 'abc' - */ - function pad(string, length, chars) { - string = toString(string); - length = toInteger(length); - - var strLength = stringSize(string); - if (!length || strLength >= length) { - return string; - } - var mid = (length - strLength) / 2, - leftLength = nativeFloor(mid), - rightLength = nativeCeil(mid); - - return createPadding('', leftLength, chars) + string + createPadding('', rightLength, chars); - } - - /** - * Pads `string` on the right side if it's shorter than `length`. Padding - * characters are truncated if they exceed `length`. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to pad. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padded string. - * @example - * - * _.padEnd('abc', 6); - * // => 'abc ' - * - * _.padEnd('abc', 6, '_-'); - * // => 'abc_-_' - * - * _.padEnd('abc', 3); - * // => 'abc' - */ - function padEnd(string, length, chars) { - string = toString(string); - return string + createPadding(string, length, chars); - } - - /** - * Pads `string` on the left side if it's shorter than `length`. Padding - * characters are truncated if they exceed `length`. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to pad. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padded string. - * @example - * - * _.padStart('abc', 6); - * // => ' abc' - * - * _.padStart('abc', 6, '_-'); - * // => '_-_abc' - * - * _.padStart('abc', 3); - * // => 'abc' - */ - function padStart(string, length, chars) { - string = toString(string); - return createPadding(string, length, chars) + string; - } - - /** - * Converts `string` to an integer of the specified radix. If `radix` is - * `undefined` or `0`, a `radix` of `10` is used unless `value` is a hexadecimal, - * in which case a `radix` of `16` is used. - * - * **Note:** This method aligns with the [ES5 implementation](https://es5.github.io/#E) - * of `parseInt`. - * - * @static - * @memberOf _ - * @category String - * @param {string} string The string to convert. - * @param {number} [radix] The radix to interpret `value` by. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {number} Returns the converted integer. - * @example - * - * _.parseInt('08'); - * // => 8 - * - * _.map(['6', '08', '10'], _.parseInt); - * // => [6, 8, 10] - */ - function parseInt(string, radix, guard) { - // Chrome fails to trim leading whitespace characters. - // See https://code.google.com/p/v8/issues/detail?id=3109 for more details. - if (guard || radix == null) { - radix = 0; - } else if (radix) { - radix = +radix; - } - string = toString(string).replace(reTrim, ''); - return nativeParseInt(string, radix || (reHasHexPrefix.test(string) ? 16 : 10)); - } - - /** - * Repeats the given string `n` times. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to repeat. - * @param {number} [n=0] The number of times to repeat the string. - * @returns {string} Returns the repeated string. - * @example - * - * _.repeat('*', 3); - * // => '***' - * - * _.repeat('abc', 2); - * // => 'abcabc' - * - * _.repeat('abc', 0); - * // => '' - */ - function repeat(string, n) { - string = toString(string); - n = toInteger(n); - - var result = ''; - if (!string || n < 1 || n > MAX_SAFE_INTEGER) { - return result; - } - // Leverage the exponentiation by squaring algorithm for a faster repeat. - // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details. - do { - if (n % 2) { - result += string; - } - n = nativeFloor(n / 2); - string += string; - } while (n); - - return result; - } - - /** - * Replaces matches for `pattern` in `string` with `replacement`. - * - * **Note:** This method is based on [`String#replace`](https://mdn.io/String/replace). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to modify. - * @param {RegExp|string} pattern The pattern to replace. - * @param {Function|string} replacement The match replacement. - * @returns {string} Returns the modified string. - * @example - * - * _.replace('Hi Fred', 'Fred', 'Barney'); - * // => 'Hi Barney' - */ - function replace() { - var args = arguments, - string = toString(args[0]); - - return args.length < 3 ? string : string.replace(args[1], args[2]); - } - - /** - * Converts `string` to [snake case](https://en.wikipedia.org/wiki/Snake_case). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the snake cased string. - * @example - * - * _.snakeCase('Foo Bar'); - * // => 'foo_bar' - * - * _.snakeCase('fooBar'); - * // => 'foo_bar' - * - * _.snakeCase('--foo-bar'); - * // => 'foo_bar' - */ - var snakeCase = createCompounder(function(result, word, index) { - return result + (index ? '_' : '') + word.toLowerCase(); - }); - - /** - * Splits `string` by `separator`. - * - * **Note:** This method is based on [`String#split`](https://mdn.io/String/split). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to split. - * @param {RegExp|string} separator The separator pattern to split by. - * @param {number} [limit] The length to truncate results to. - * @returns {Array} Returns the new array of string segments. - * @example - * - * _.split('a-b-c', '-', 2); - * // => ['a', 'b'] - */ - function split(string, separator, limit) { - return toString(string).split(separator, limit); - } - - /** - * Converts `string` to [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the start cased string. - * @example - * - * _.startCase('--foo-bar'); - * // => 'Foo Bar' - * - * _.startCase('fooBar'); - * // => 'Foo Bar' - * - * _.startCase('__foo_bar__'); - * // => 'Foo Bar' - */ - var startCase = createCompounder(function(result, word, index) { - return result + (index ? ' ' : '') + capitalize(word); - }); - - /** - * Checks if `string` starts with the given target string. - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The string to search. - * @param {string} [target] The string to search for. - * @param {number} [position=0] The position to search from. - * @returns {boolean} Returns `true` if `string` starts with `target`, else `false`. - * @example - * - * _.startsWith('abc', 'a'); - * // => true - * - * _.startsWith('abc', 'b'); - * // => false - * - * _.startsWith('abc', 'b', 1); - * // => true - */ - function startsWith(string, target, position) { - string = toString(string); - position = baseClamp(toInteger(position), 0, string.length); - return string.lastIndexOf(target, position) == position; - } - - /** - * Creates a compiled template function that can interpolate data properties - * in "interpolate" delimiters, HTML-escape interpolated data properties in - * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data - * properties may be accessed as free variables in the template. If a setting - * object is provided it takes precedence over `_.templateSettings` values. - * - * **Note:** In the development build `_.template` utilizes - * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) - * for easier debugging. - * - * For more information on precompiling templates see - * [lodash's custom builds documentation](https://lodash.com/custom-builds). - * - * For more information on Chrome extension sandboxes see - * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). - * - * @static - * @memberOf _ - * @category String - * @param {string} [string=''] The template string. - * @param {Object} [options] The options object. - * @param {RegExp} [options.escape] The HTML "escape" delimiter. - * @param {RegExp} [options.evaluate] The "evaluate" delimiter. - * @param {Object} [options.imports] An object to import into the template as free variables. - * @param {RegExp} [options.interpolate] The "interpolate" delimiter. - * @param {string} [options.sourceURL] The sourceURL of the template's compiled source. - * @param {string} [options.variable] The data object variable name. - * @param- {Object} [guard] Enables use as an iteratee for functions like `_.map`. - * @returns {Function} Returns the compiled template function. - * @example - * - * // using the "interpolate" delimiter to create a compiled template - * var compiled = _.template('hello <%= user %>!'); - * compiled({ 'user': 'fred' }); - * // => 'hello fred!' - * - * // using the HTML "escape" delimiter to escape data property values - * var compiled = _.template('<%- value %>'); - * compiled({ 'value': ' \ No newline at end of file diff --git a/public/app/views/records/classification.html.erb b/public/app/views/records/classification.html.erb index d498945b8d..70e725f1f2 100644 --- a/public/app/views/records/classification.html.erb +++ b/public/app/views/records/classification.html.erb @@ -18,27 +18,12 @@
    • - <% show_term = lambda do |term| %> -
    • <%= link_to("#{term['identifier']}. #{term['title']}", - { - :controller => :search, - :action => :search, - :term_map => {term['record_uri'] => term['title']}.to_json - }.merge(params_for_search({ - "add_filter_term" => {"classification_uris" => term['record_uri']}.to_json - }))) %>
    • - <% if !term['children'].empty? %> -
        - <% term['children'].each do |node| %> - <% show_term.call(node) %> - <% end %> -
      - <% end %> - <% end %> + <%= render_aspace_partial :partial => "components", :locals => { + :record_type => "classification", + :root_uri => @classification.uri, + :current_node_uri => @classification.uri, + } %> -
        - <% show_term.call(@tree_view['whole_tree']) %> -
      diff --git a/public/app/views/records/digital_object.html.erb b/public/app/views/records/digital_object.html.erb index 2fd1b883c6..0dd7bd52f9 100644 --- a/public/app/views/records/digital_object.html.erb +++ b/public/app/views/records/digital_object.html.erb @@ -49,7 +49,8 @@ <%= render_aspace_partial :partial => "components", :locals => { :record_type => "digital_object", :root_uri => @digital_object.uri, - :show_search => true + :show_search => true, + :current_node_uri => @digital_object.uri, } %> diff --git a/public/app/views/records/digital_object_component.html.erb b/public/app/views/records/digital_object_component.html.erb index ab4c54e113..3f8a5e1602 100644 --- a/public/app/views/records/digital_object_component.html.erb +++ b/public/app/views/records/digital_object_component.html.erb @@ -47,7 +47,8 @@ <%= render_aspace_partial :partial => "components", :locals => { :record_type => "digital_object_component", - :root_uri => @digital_object_component.uri, + :root_uri => @digital_object_component.digital_object['ref'], + :current_node_uri => @digital_object_component.uri, } %> diff --git a/public/app/views/records/resource.html.erb b/public/app/views/records/resource.html.erb index ed1e45ec43..49dbf25994 100644 --- a/public/app/views/records/resource.html.erb +++ b/public/app/views/records/resource.html.erb @@ -65,6 +65,7 @@ :record_type => "resource", :root_uri => @resource.uri, :show_search => true, + :current_node_uri => @resource.uri, } %> diff --git a/public/config/application.rb b/public/config/application.rb index 4dba6106be..0db403d2b3 100644 --- a/public/config/application.rb +++ b/public/config/application.rb @@ -8,8 +8,6 @@ require 'config/config-distribution' require 'asutils' -require "rails_config_bug_workaround" - if defined?(Bundler) # If you precompile assets before deploying to production, use this line Bundler.require(*Rails.groups(:assets => %w(development test))) @@ -19,6 +17,11 @@ module ArchivesSpacePublic class Application < Rails::Application + + def self.extend_aspace_routes(routes_file) + ArchivesSpacePublic::Application.config.paths['config/routes.rb'].concat([routes_file]) + end + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. @@ -47,22 +50,23 @@ class Application < Rails::Application # THIS DOESN'T WORK FOR DISTRIBUTED WAR... NEED TO SETUP LOCALE SHARING #config.i18n.load_path += Dir[Rails.root.join('..', 'frontend','config', 'locales', '**', '*.{rb,yml}')] - config.i18n.default_locale = AppConfig[:locale] # Load the shared 'locales' - ASUtils.find_locales_directories.map{|locales_directory| File.join(locales_directory)}.reject { |dir| !Dir.exists?(dir) }.each do |locales_directory| - config.i18n.load_path += Dir[File.join(locales_directory, '**' , '*.{rb,yml}')] + ASUtils.find_locales_directories.map{|locales_directory| File.join(locales_directory)}.reject { |dir| !Dir.exist?(dir) }.each do |locales_directory| + I18n.load_path += Dir[File.join(locales_directory, '**' , '*.{rb,yml}')] end # Override with any local locale files - config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] + I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] # Allow overriding of the i18n locales via the local folder(s) if not ASUtils.find_local_directories.blank? - ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'locales')}.reject { |dir| !Dir.exists?(dir) }.each do |locales_override_directory| - config.i18n.load_path += Dir[File.join(locales_override_directory, '**' , '*.{rb,yml}')] + ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'locales')}.reject { |dir| !Dir.exist?(dir) }.each do |locales_override_directory| + I18n.load_path += Dir[File.join(locales_override_directory, '**' , '*.{rb,yml}')] end end + config.i18n.default_locale = AppConfig[:locale] + # config.i18n.default_locale = :de # Configure the default encoding used in templates for Ruby 1.9. @@ -111,7 +115,7 @@ class SessionExpired < StandardError # Load plugin init.rb files (if present) ASUtils.find_local_directories('public').each do |dir| init_file = File.join(dir, "plugin_init.rb") - if File.exists?(init_file) + if File.exist?(init_file) load init_file end end diff --git a/public/config/boot.rb b/public/config/boot.rb index 3d1559cb6d..67c447d556 100644 --- a/public/config/boot.rb +++ b/public/config/boot.rb @@ -7,4 +7,4 @@ require 'aspace_gems' ASpaceGems.setup -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) diff --git a/public/config/environments/development.rb b/public/config/environments/development.rb index b2e2e2be6c..3f7a6a1817 100644 --- a/public/config/environments/development.rb +++ b/public/config/environments/development.rb @@ -9,8 +9,10 @@ # Log error messages when you accidentally call methods on nil. config.whiny_nils = true + config.eager_load = true + # Show full error reports and disable caching - config.consider_all_requests_local = true + config.consider_all_requests_local = false config.action_controller.perform_caching = false # Don't care if the mailer can't send @@ -22,12 +24,10 @@ # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin - # config.threadsafe! - # DEVELOPMENT ONLY - Allow overriding of the static resources via the local folder(s) # N.B. that is supported by the launcher.rb when in production if not ASUtils.find_local_directories.blank? - ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'assets')}.reject { |dir| !Dir.exists?(dir) }.each do |static_directory| + ASUtils.find_local_directories.map{|local_dir| File.join(local_dir, 'public', 'assets')}.reject { |dir| !Dir.exist?(dir) }.each do |static_directory| config.assets.paths.unshift(static_directory) end end diff --git a/public/config/environments/production.rb b/public/config/environments/production.rb index ee8cbd8ed4..67d70c5dd9 100644 --- a/public/config/environments/production.rb +++ b/public/config/environments/production.rb @@ -13,6 +13,8 @@ # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_assets = true + config.eager_load = true + # Compress JavaScripts and CSS config.assets.compress = true config.assets.js_compressor = ASpaceCompressor.new(:js) @@ -27,7 +29,7 @@ # If a prefix has been specified, use it! config.assets.prefix = AppConfig[:public_proxy_prefix] + "assets" - config.assets.manifest = File.join(Rails.public_path, "assets") + #config.assets.manifest = File.join(Rails.public_path, "assets") # Specifies the header that your server uses for sending files # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache @@ -61,9 +63,6 @@ # Disable delivery errors, bad email addresses will be ignored # config.action_mailer.raise_delivery_errors = false - # Enable threaded mode - config.threadsafe! - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true diff --git a/public/config/environments/test.rb b/public/config/environments/test.rb index 3fc902b381..09f4725837 100644 --- a/public/config/environments/test.rb +++ b/public/config/environments/test.rb @@ -11,11 +11,13 @@ config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" + config.eager_load = true + # Log error messages when you accidentally call methods on nil config.whiny_nils = true # Show full error reports and disable caching - config.consider_all_requests_local = true + config.consider_all_requests_local = false config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates diff --git a/public/config/initializers/secret_token.rb b/public/config/initializers/secret_token.rb index efa38de3c5..1eef89e8ae 100644 --- a/public/config/initializers/secret_token.rb +++ b/public/config/initializers/secret_token.rb @@ -2,4 +2,5 @@ if !ENV['DISABLE_STARTUP'] ArchivesSpacePublic::Application.config.secret_token = Digest::SHA1.hexdigest(AppConfig[:public_cookie_secret]) + ArchivesSpacePublic::Application.config.secret_key_base = Digest::SHA1.hexdigest(AppConfig[:public_cookie_secret]) end diff --git a/public/config/locales/en.yml b/public/config/locales/en.yml index c61f1a6db5..af6aa444db 100644 --- a/public/config/locales/en.yml +++ b/public/config/locales/en.yml @@ -169,3 +169,8 @@ en: record_tree: record_tree_tab: Record Tree search_tab: Search Components + + classification: + _public: + section: + components: Terms diff --git a/public/config/routes.rb b/public/config/routes.rb index a070b9c4ba..cac782db23 100644 --- a/public/config/routes.rb +++ b/public/config/routes.rb @@ -16,6 +16,24 @@ match 'repositories/:repo_id/accessions/:id' => 'records#accession', :via => [:get] match 'agents/:id' => 'records#agent', :via => [:get] + get "repositories/:repo_id/resources/:id/tree/root" => 'records#resource_tree_root' + get "repositories/:repo_id/resources/:id/tree/waypoint" => 'records#resource_tree_waypoint' + get "repositories/:repo_id/resources/:id/tree/node" => 'records#resource_tree_node' + get "repositories/:repo_id/resources/:id/tree/node_from_root" => 'records#resource_tree_node_from_root' + + get "repositories/:repo_id/digital_objects/:id/tree/root" => 'records#digital_object_tree_root' + get "repositories/:repo_id/digital_objects/:id/tree/waypoint" => 'records#digital_object_tree_waypoint' + get "repositories/:repo_id/digital_objects/:id/tree/node" => 'records#digital_object_tree_node' + get "repositories/:repo_id/digital_objects/:id/tree/node_from_root" => 'records#digital_object_tree_node_from_root' + + get "repositories/:repo_id/classifications/:id/tree/root" => 'records#classification_tree_root' + get "repositories/:repo_id/classifications/:id/tree/waypoint" => 'records#classification_tree_waypoint' + get "repositories/:repo_id/classifications/:id/tree/node" => 'records#classification_tree_node' + get "repositories/:repo_id/classifications/:id/tree/node_from_root" => 'records#classification_tree_node_from_root' + + get "repositories/:repo_id/classifications/:id/search" => 'records#classification_search' + get "repositories/:repo_id/classification_terms/:id/search" => 'records#classification_term_search' + match 'repositories' => 'search#repository', :via => [:get] match 'subjects/:id' => 'search#subject', :via => [:get] root :to => "site#index" diff --git a/public/config/warble.rb b/public/config/warble.rb index d025d46357..eb10be4c53 100644 --- a/public/config/warble.rb +++ b/public/config/warble.rb @@ -126,7 +126,7 @@ config.webxml.booter = :rails # Set JRuby to run in 1.9 mode. - config.webxml.jruby.compat.version = "1.9" + # config.webxml.jruby.compat.version = "1.9" # When using the :rack booter, "Rackup" script to use. # - For 'rackup.path', the value points to the location of the rackup diff --git a/public/lib/tasks/rake_jruby_complete_patch.rake b/public/lib/tasks/rake_jruby_complete_patch.rake index 27ae4a3820..c9d12be079 100644 --- a/public/lib/tasks/rake_jruby_complete_patch.rake +++ b/public/lib/tasks/rake_jruby_complete_patch.rake @@ -10,7 +10,7 @@ classpath << Dir.glob(File.join(Rails.root, "..", "build", "jruby*complete*.jar" $rake_cmd = ["java", "-XX:MaxPermSize=128m", "-Xmx256m", "-cp", classpath.join(java.io.File.pathSeparator), - "org.jruby.Main", "--1.9", "-X-C", "-S", "rake"] + "org.jruby.Main", "-X-C", "-S", "rake"] namespace :assets do diff --git a/reports/Accessions/AccessionsAcquiredReport/AccessionsAcquiredReport.jrxml b/reports/Accessions/AccessionsAcquiredReport/AccessionsAcquiredReport.jrxml deleted file mode 100644 index dabf62319e..0000000000 --- a/reports/Accessions/AccessionsAcquiredReport/AccessionsAcquiredReport.jrxml +++ /dev/null @@ -1,477 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="277" splitType="Stretch"> - <textField evaluationTime="Report" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField" positionType="Float" x="443" y="247" width="82" height="15" forecolor="#000000" uuid="1bf33035-daeb-4fe3-8a9d-f396144956c0"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-10" positionType="Float" x="368" y="215" width="82" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="2b231ffe-adca-4ad5-a4d1-32f20109e5d8"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-14" x="13" y="150" width="506" height="58" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="2c8b09ec-0c78-419e-8ec2-3743e6275538"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Acquired Accessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-19" positionType="Float" x="266" y="231" width="113" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="8af002d9-45ea-41d5-901d-421faee179c9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessioned Between:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-22" positionType="Float" x="266" y="215" width="102" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="d122bf4b-79df-4367-9931-273e6fcb76fe"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-18" positionType="Float" x="266" y="247" width="176" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="3472ac98-866e-4207-8279-f44d1ea96b29"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Aquired Accessions:]]></text> - </staticText> - <staticText> - <reportElement positionType="Float" x="434" y="231" width="14" height="15" uuid="ae9c5297-1717-4a46-838a-04e5dd0d1fc5"/> - <textElement textAlignment="Center"> - <font fontName="Arial" size="10"/> - </textElement> - <text><![CDATA[&]]></text> - </staticText> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-12" positionType="Float" isPrintRepeatedValues="false" x="448" y="231" width="55" height="15" forecolor="#000000" uuid="12f1730f-0192-45db-ad2a-3a046d6a4339"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MAX_DATE}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-11" positionType="Float" isPrintRepeatedValues="false" x="379" y="231" width="55" height="15" forecolor="#000000" uuid="6dd2eb10-71d6-4659-a872-daeda19baa93"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Right"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MIN_DATE}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsAcquiredReport/report_config.yml b/reports/Accessions/AccessionsAcquiredReport/report_config.yml deleted file mode 100644 index 02a8dfe485..0000000000 --- a/reports/Accessions/AccessionsAcquiredReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_acquired -description: "Displays a list of all accessions acquired in a specified time period. Report contains accession number, title, extent, accession date, container summary, cataloged, date processed, rights transferred and the total number and physical extent." diff --git a/reports/Accessions/AccessionsCatalogedReport/AccessionsCatalogedReport.jrxml b/reports/Accessions/AccessionsCatalogedReport/AccessionsCatalogedReport.jrxml deleted file mode 100644 index bf019311f5..0000000000 --- a/reports/Accessions/AccessionsCatalogedReport/AccessionsCatalogedReport.jrxml +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="270" splitType="Stretch"> - <staticText> - <reportElement key="staticText-18" positionType="Float" x="266" y="234" width="116" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="b7f4aedf-139e-4661-b2e7-6094d5ac23ff"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Cataloged Accessions:]]></text> - </staticText> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField" positionType="Float" x="382" y="234" width="42" height="15" forecolor="#000000" uuid="f987fe13-fa39-41b6-8c5f-1bbfc5e5522b"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{CATALOGED_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="###0.00" isBlankWhenNull="false"> - <reportElement key="textField-11" positionType="Float" x="452" y="249" width="53" height="15" forecolor="#000000" uuid="6a7188de-39f4-45c0-bd3d-dd9378f6dfd2"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_CATALOGED_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-20" positionType="Float" x="266" y="249" width="185" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="f5676d6c-ce03-4ed1-bfc1-2e3d1f3341ae"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Cataloged Accessions:]]></text> - </staticText> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-10" x="10" y="150" width="510" height="60" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="86dec6b8-5f16-4982-b2e2-bc6b664ad9b6"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Cataloged Accessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-7" positionType="Float" x="419" y="219" width="88" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="9fa355fb-e7ae-46ac-a016-53f01be50746"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-22" positionType="Float" x="266" y="219" width="150" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="8573005d-0423-4f3e-93b7-539ddce1abf3"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Reviewed:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsCatalogedReport/report_config.yml b/reports/Accessions/AccessionsCatalogedReport/report_config.yml deleted file mode 100644 index b47e22d10e..0000000000 --- a/reports/Accessions/AccessionsCatalogedReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_cataloged -description: "Displays only those accessions that have been cataloged. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected that are checked as cataloged, and the total extent number for those records cataloged." diff --git a/reports/Accessions/AccessionsCatalogedReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsCatalogedReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsCatalogedReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsDeaccessionsListReport/AccessionsDeaccessionsListReport.jrxml b/reports/Accessions/AccessionsDeaccessionsListReport/AccessionsDeaccessionsListReport.jrxml deleted file mode 100644 index 5afd5c0cf9..0000000000 --- a/reports/Accessions/AccessionsDeaccessionsListReport/AccessionsDeaccessionsListReport.jrxml +++ /dev/null @@ -1,432 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="298" splitType="Stretch"> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField" x="402" y="258" width="88" height="15" forecolor="#000000" uuid="ab092e2a-06fb-4e8b-b8a5-223f719a0013"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-10" positionType="Float" x="366" y="226" width="88" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="94a9fb29-6a38-47ad-a0bf-0d664da355b3"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="##0.00" isBlankWhenNull="true"> - <reportElement key="textField" positionType="Float" isPrintRepeatedValues="false" x="414" y="274" width="88" height="15" forecolor="#000000" uuid="9eb17f2e-65c8-4c8e-ba09-faf5dab736fd"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10"/> - </textElement> - <textFieldExpression><![CDATA[$V{DEACCESSION_EXTENT_SUM}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-14" x="14" y="150" width="503" height="62" forecolor="#000000" backcolor="#FFFFFF" uuid="ad3c823a-b596-4540-aaf0-cda68338b778"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accessions Acquired and Linked Deaccession Records" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-19" positionType="Float" x="265" y="242" width="113" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="c3100394-62b2-479c-b19e-68d1c3e4ed48"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessioned Between:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-18" positionType="Float" x="265" y="274" width="146" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="b394350f-3ade-4390-b9f1-064e02e030c0"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Deaccessions:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-22" positionType="Float" x="265" y="226" width="99" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="acc6f55e-2ae3-4452-b6e0-97257a1d8d16"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - <staticText> - <reportElement positionType="Float" x="434" y="242" width="14" height="15" uuid="15e75116-87be-4225-b8fb-2046fed6d944"/> - <textElement textAlignment="Center"> - <font fontName="Arial" size="10"/> - </textElement> - <text><![CDATA[&]]></text> - </staticText> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-12" isPrintRepeatedValues="false" x="448" y="242" width="55" height="15" forecolor="#000000" uuid="b073c327-154c-4920-b6d6-c2ac54a4eb9f"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MAX_DATE}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-11" positionType="Float" isPrintRepeatedValues="false" x="379" y="242" width="55" height="15" forecolor="#000000" uuid="55109aff-705f-4d31-81c7-94e24dd499c1"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Right"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MIN_DATE}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-18" positionType="Float" x="265" y="258" width="135" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="f6cb06ce-d876-4527-be4e-4a2794e4e668"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Accessions:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsDeaccessionsListReport/report_config.yml b/reports/Accessions/AccessionsDeaccessionsListReport/report_config.yml deleted file mode 100644 index 144ae46f82..0000000000 --- a/reports/Accessions/AccessionsDeaccessionsListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_deaccessions_list -description: "Displays a list of accession record(s) and linked deaccession record(s). Report contains accession number, title, extent, accession date, container summary, cataloged, date processed, rights transferred, linked deaccessions and total extent of all deaccessions." diff --git a/reports/Accessions/AccessionsDeaccessionsListReport/sub_accessionsDeaccessions.jrxml b/reports/Accessions/AccessionsDeaccessionsListReport/sub_accessionsDeaccessions.jrxml deleted file mode 100644 index ab389996ec..0000000000 --- a/reports/Accessions/AccessionsDeaccessionsListReport/sub_accessionsDeaccessions.jrxml +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsInventoryReport/AccessionsInventoryReport.jrxml b/reports/Accessions/AccessionsInventoryReport/AccessionsInventoryReport.jrxml deleted file mode 100644 index e1f33377a5..0000000000 --- a/reports/Accessions/AccessionsInventoryReport/AccessionsInventoryReport.jrxml +++ /dev/null @@ -1,444 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="249" splitType="Stretch"> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField" mode="Transparent" x="418" y="215" width="50" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="9605dfac-b1b2-43f7-acc9-a8b70e2425c3"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-22" mode="Transparent" x="409" y="230" width="50" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="dea6b54a-c50e-49f9-a769-21152e4fd107"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA["" + $V{TEST_INVENTORY}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-20" mode="Transparent" x="14" y="151" width="503" height="54" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="f144b6c0-9aa6-4f84-ad00-4a572ad3a1ba"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accessions with Inventories" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-22" mode="Transparent" x="266" y="215" width="150" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="daf387d7-2882-4a48-8445-e0db79dcfc87"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Reviewed:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-21" mode="Transparent" x="266" y="230" width="140" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="a077fe9b-e975-4a24-a440-cc8f8f0c04c4"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessions with Inventories:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsInventoryReport/report_config.yml b/reports/Accessions/AccessionsInventoryReport/report_config.yml deleted file mode 100644 index 9f7b615afa..0000000000 --- a/reports/Accessions/AccessionsInventoryReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_inventory -description: "Displays only those accession records with an inventory. Report contains accession number, linked resources, title, extent, accession date, container summary, and inventory. " diff --git a/reports/Accessions/AccessionsInventoryReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsInventoryReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsInventoryReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsLocationsListReport/AccessionsLocationsListReport.jrxml b/reports/Accessions/AccessionsLocationsListReport/AccessionsLocationsListReport.jrxml deleted file mode 100644 index 681e3e624c..0000000000 --- a/reports/Accessions/AccessionsLocationsListReport/AccessionsLocationsListReport.jrxml +++ /dev/null @@ -1,399 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="298" splitType="Stretch"> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField" x="402" y="258" width="88" height="15" forecolor="#000000" uuid="fc294057-86b5-41d7-ae38-628d675d3973"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-10" positionType="Float" x="366" y="226" width="88" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="bc179f4a-46a3-4a10-930b-3cf334cd3865"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-14" x="14" y="150" width="503" height="62" forecolor="#000000" backcolor="#FFFFFF" uuid="a87f21f6-34a3-45c5-9af2-972fab4039b2"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accessions and Locations List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-19" positionType="Float" x="265" y="242" width="113" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="6c1878d2-c303-4ecb-94c8-97401ab0d14b"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessioned Between:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-22" positionType="Float" x="265" y="226" width="99" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="e8c4abb8-c66f-484c-8b4d-fb10e6f40aca"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - <staticText> - <reportElement positionType="Float" x="434" y="242" width="14" height="15" uuid="c0ffafdf-89d6-4394-a848-7d49fc8dfbf5"/> - <textElement textAlignment="Center"> - <font fontName="Arial" size="10"/> - </textElement> - <text><![CDATA[&]]></text> - </staticText> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-12" isPrintRepeatedValues="false" x="448" y="242" width="55" height="15" forecolor="#000000" uuid="d84a8575-d2ff-4308-ace6-0cc943febe85"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MAX_DATE}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-11" positionType="Float" isPrintRepeatedValues="false" x="379" y="242" width="55" height="15" forecolor="#000000" uuid="d09db320-2c1d-423d-bdcf-cf9fc0017e53"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Right"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MIN_DATE}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-18" positionType="Float" x="265" y="258" width="135" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="e812a892-bbbd-4229-9c26-85fa595e4956"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Accessions:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsLocationsListReport/report_config.yml b/reports/Accessions/AccessionsLocationsListReport/report_config.yml deleted file mode 100644 index e4697cd82d..0000000000 --- a/reports/Accessions/AccessionsLocationsListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_locations_list -description: "Displays accessions(s) and all specified location information. Report contains title, accession number, cataloged, date range, and assigned locations." diff --git a/reports/Accessions/AccessionsLocationsListReport/sub_accessionsLocations.jrxml b/reports/Accessions/AccessionsLocationsListReport/sub_accessionsLocations.jrxml deleted file mode 100644 index 825c976fca..0000000000 --- a/reports/Accessions/AccessionsLocationsListReport/sub_accessionsLocations.jrxml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsProcessedReport/AccessionsProcessedReport.jrxml b/reports/Accessions/AccessionsProcessedReport/AccessionsProcessedReport.jrxml deleted file mode 100644 index 6cabaa687f..0000000000 --- a/reports/Accessions/AccessionsProcessedReport/AccessionsProcessedReport.jrxml +++ /dev/null @@ -1,427 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="312" splitType="Stretch"> - <staticText> - <reportElement key="staticText-21" x="255" y="232" width="113" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="21002f01-a9b6-40e5-a7d3-8aacb576ae22"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Processed Accessions:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="###0.00" isBlankWhenNull="true"> - <reportElement key="textField-9" x="443" y="264" width="79" height="15" forecolor="#000000" uuid="7542ef3e-8ae6-45dd-aa48-c3f67f5233b8"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_PROCESSED_SUM}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-13" x="405" y="216" width="36" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="349c6ad5-0bfa-4752-ae20-ffc4e298744c"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-14" x="371" y="232" width="49" height="15" forecolor="#000000" uuid="c27778e5-30ca-4418-bfff-2735fef30279"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{PROCESSED_TEST} + 1]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-18" x="255" y="264" width="186" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="912bf8be-1145-4a83-a000-9a47d95212cd"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Processed Accessions:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-22" x="255" y="216" width="150" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="4ead7f97-e6bb-487a-b494-b3e25d31a9ab"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Reviewed:]]></text> - </staticText> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-8" x="23" y="150" width="493" height="61" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="ec416a34-f267-4c59-a84e-b205f364cfcc"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Processed Accessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-19" x="255" y="248" width="113" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="5603cd9f-e4f8-4642-9a03-76acf934203b"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessioned Between:]]></text> - </staticText> - <staticText> - <reportElement x="426" y="248" width="14" height="15" uuid="9b150ed7-8ac4-497e-bc71-a9c42b77d859"/> - <textElement textAlignment="Center"> - <font fontName="Arial" size="10"/> - </textElement> - <text><![CDATA[&]]></text> - </staticText> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-12" isPrintRepeatedValues="false" x="440" y="248" width="55" height="15" forecolor="#000000" uuid="c37b18cf-79a7-477a-be11-25de20c6d8c9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MAX_DATE}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-11" isPrintRepeatedValues="false" x="371" y="248" width="55" height="15" forecolor="#000000" uuid="e0b7c706-8326-4c12-bdb7-d3f7b3acb2d9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Right"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MIN_DATE}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsProcessedReport/report_config.yml b/reports/Accessions/AccessionsProcessedReport/report_config.yml deleted file mode 100644 index 20cf1b90fe..0000000000 --- a/reports/Accessions/AccessionsProcessedReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_processed -description: "Displays only those accession(s) that have been processed based on the date processed field. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected with a date processed, and the total extent number for those records with date processed." diff --git a/reports/Accessions/AccessionsProcessedReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsProcessedReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsProcessedReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsProductionReport/AccessionsProductionReport.jrxml b/reports/Accessions/AccessionsProductionReport/AccessionsProductionReport.jrxml deleted file mode 100644 index acefc9dd62..0000000000 --- a/reports/Accessions/AccessionsProductionReport/AccessionsProductionReport.jrxml +++ /dev/null @@ -1,329 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="180" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-13" mode="Transparent" x="0" y="0" width="532" height="33" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="bfe11c1d-5285-43c2-b1c9-f2dd5c488184"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accession Production Report" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Group" evaluationGroup="SORT_GROUP" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField" x="231" y="73" width="116" height="25" forecolor="#000000" uuid="dbebc7bd-6df2-4e7e-a3df-74925e11ac89"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement verticalAlignment="Middle"> - <font fontName="Arial" size="12" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-21" mode="Transparent" x="10" y="98" width="174" height="25" forecolor="#000000" backcolor="#FFFFFF" uuid="ecf18707-de14-4899-9f2c-2afaa688936e"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle" rotation="None"> - <font fontName="Arial" size="12" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Selected:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-10" mode="Transparent" x="184" y="98" width="116" height="25" forecolor="#000000" backcolor="#FFFFFF" uuid="e6c8f9dd-401e-47d9-8f71-8ba93b3ab8ed"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle" rotation="None"> - <font fontName="Arial" size="12" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-22" mode="Transparent" x="10" y="73" width="221" height="25" forecolor="#000000" backcolor="#FFFFFF" uuid="6463be48-3cdb-40ec-a475-440f20c94fa2"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle" rotation="None"> - <font fontName="Arial" size="12" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Selected Accessions:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField-11" x="234" y="123" width="116" height="25" forecolor="#000000" uuid="0a36bab8-d79c-43be-9098-5d33870132c6"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement verticalAlignment="Middle"> - <font fontName="Arial" size="12" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_CATALOGED_SUM}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField-12" x="238" y="148" width="116" height="25" forecolor="#000000" uuid="3b90e3d0-7828-4940-b44d-b73bef7fa600"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement verticalAlignment="Middle"> - <font fontName="Arial" size="12" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_PROCESSED_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement x="209" y="48" width="21" height="25" uuid="dbf23d41-9335-4fba-9cfd-b0da53242927"/> - <textElement textAlignment="Center" verticalAlignment="Middle"> - <font fontName="Arial" size="12"/> - </textElement> - <text><![CDATA[&]]></text> - </staticText> - <staticText> - <reportElement key="staticText-19" mode="Transparent" x="10" y="48" width="136" height="25" forecolor="#000000" backcolor="#FFFFFF" uuid="3dcb6d3d-c699-49d1-8971-d4a4707b8b09"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle" rotation="None"> - <font fontName="Arial" size="12" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessioned Between:]]></text> - </staticText> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-11" x="147" y="48" width="62" height="25" forecolor="#000000" uuid="ba72d330-15ec-48d3-af7f-6d74465637d3"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Right" verticalAlignment="Middle"> - <font fontName="Arial" size="12" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MIN_DATE}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-12" x="230" y="48" width="62" height="25" forecolor="#000000" uuid="3112864f-4a99-48ab-a305-ed4896373557"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle"> - <font fontName="Arial" size="12" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MAX_DATE}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-18" mode="Transparent" x="10" y="148" width="228" height="25" forecolor="#000000" backcolor="#FFFFFF" uuid="f967ddb3-d36d-4d36-9af5-41969918e2a0"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle" rotation="None"> - <font fontName="Arial" size="12" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Processed Accessions:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-18" mode="Transparent" x="10" y="123" width="221" height="25" forecolor="#000000" backcolor="#FFFFFF" uuid="2b28af40-3886-4e97-afd3-896296ddba58"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Middle" rotation="None"> - <font fontName="Arial" size="12" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Cataloged Accessions:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsProductionReport/report_config.yml b/reports/Accessions/AccessionsProductionReport/report_config.yml deleted file mode 100644 index 57130e7ec5..0000000000 --- a/reports/Accessions/AccessionsProductionReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_production -description: "Displays accessions that have been accessioned, processed, and cataloged during a specified time period. Produces a summary statement of the total number of accessions, the total extent, total extent processed, and extent cataloged within the specified date range." diff --git a/reports/Accessions/AccessionsReceiptReport/AccessionsReceiptReport.jrxml b/reports/Accessions/AccessionsReceiptReport/AccessionsReceiptReport.jrxml deleted file mode 100644 index c81026d442..0000000000 --- a/reports/Accessions/AccessionsReceiptReport/AccessionsReceiptReport.jrxml +++ /dev/null @@ -1,278 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsReceiptReport/report_config.yml b/reports/Accessions/AccessionsReceiptReport/report_config.yml deleted file mode 100644 index 9b66c363c0..0000000000 --- a/reports/Accessions/AccessionsReceiptReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_receipt -description: "Displays a receipt indicating accessioning of materials. Report contains accession number, title, extent, accession date, and repository." diff --git a/reports/Accessions/AccessionsRecordReport/AccessionsRecordReport.jrxml b/reports/Accessions/AccessionsRecordReport/AccessionsRecordReport.jrxml deleted file mode 100644 index b0e4191def..0000000000 --- a/reports/Accessions/AccessionsRecordReport/AccessionsRecordReport.jrxml +++ /dev/null @@ -1,830 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="222" splitType="Stretch"> - <staticText> - <reportElement key="staticText-2" positionType="Float" mode="Transparent" x="266" y="209" width="104" height="12" forecolor="#000000" backcolor="#FFFFFF" uuid="ab71b99c-740f-49e7-818b-f11fd7a70b82"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField" positionType="Float" mode="Transparent" x="370" y="209" width="50" height="12" forecolor="#000000" backcolor="#FFFFFF" uuid="497b6346-bbe2-4158-b8de-0474d8a4441f"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-5" mode="Transparent" x="16" y="150" width="496" height="50" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="e7fb6f00-9b7b-46b5-81c0-dfb12ca24d7d"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accession Records" : $P{ReportHeader}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRecordReport/report_config.yml b/reports/Accessions/AccessionsRecordReport/report_config.yml deleted file mode 100644 index 883780ce44..0000000000 --- a/reports/Accessions/AccessionsRecordReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_record -description: "Displays key fields for selected accession record(s)." diff --git a/reports/Accessions/AccessionsRecordReport/sub_accessionsDeaccessions.jrxml b/reports/Accessions/AccessionsRecordReport/sub_accessionsDeaccessions.jrxml deleted file mode 100644 index 7fc8513c8d..0000000000 --- a/reports/Accessions/AccessionsRecordReport/sub_accessionsDeaccessions.jrxml +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRecordReport/sub_accessionsLocations.jrxml b/reports/Accessions/AccessionsRecordReport/sub_accessionsLocations.jrxml deleted file mode 100644 index 825c976fca..0000000000 --- a/reports/Accessions/AccessionsRecordReport/sub_accessionsLocations.jrxml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRecordReport/sub_accessionsNames.jrxml b/reports/Accessions/AccessionsRecordReport/sub_accessionsNames.jrxml deleted file mode 100644 index b2b43f7473..0000000000 --- a/reports/Accessions/AccessionsRecordReport/sub_accessionsNames.jrxml +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRecordReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsRecordReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsRecordReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRecordReport/sub_accessionsSubjects.jrxml b/reports/Accessions/AccessionsRecordReport/sub_accessionsSubjects.jrxml deleted file mode 100644 index d91ade4007..0000000000 --- a/reports/Accessions/AccessionsRecordReport/sub_accessionsSubjects.jrxml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRightsTransferredReport/AccessionsRightsTransferredReport.jrxml b/reports/Accessions/AccessionsRightsTransferredReport/AccessionsRightsTransferredReport.jrxml deleted file mode 100644 index cd607a2e17..0000000000 --- a/reports/Accessions/AccessionsRightsTransferredReport/AccessionsRightsTransferredReport.jrxml +++ /dev/null @@ -1,528 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="258" splitType="Stretch"> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-7" mode="Transparent" x="416" y="218" width="50" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="40637bc0-3167-4f5a-bebc-80b04a0eec21"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-11" x="444" y="233" width="50" height="15" forecolor="#000000" uuid="f5a461a0-a5d3-4b3b-b779-4752c469f417"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{RIGHTS_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="13" y="150" width="503" height="50" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="a5933d50-39a7-424b-931a-53a46870a15a"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accessions with Rights Transferred" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-22" mode="Transparent" x="266" y="218" width="150" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="a8a38e27-d1e2-4f89-a845-0783f76f0a07"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Reviewed:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-18" mode="Transparent" x="266" y="233" width="176" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="5531623e-01e7-47ae-9d1d-3591a2a0feee"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessions with Rights Transferred:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsRightsTransferredReport/report_config.yml b/reports/Accessions/AccessionsRightsTransferredReport/report_config.yml deleted file mode 100644 index 326475152b..0000000000 --- a/reports/Accessions/AccessionsRightsTransferredReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_rights_transferred -description: "Displays only those accession(s) for which rights have been transferred. Report contains accession number, linked resources, title, extent, cataloged, date processed, access restrictions, use restrictions, rights transferred and a count of the number of records selected with rights transferred." diff --git a/reports/Accessions/AccessionsRightsTransferredReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsRightsTransferredReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsRightsTransferredReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/AccessionsSubjectsNamesClassificationsListReport.jrxml b/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/AccessionsSubjectsNamesClassificationsListReport.jrxml deleted file mode 100644 index f4ee967a35..0000000000 --- a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/AccessionsSubjectsNamesClassificationsListReport.jrxml +++ /dev/null @@ -1,349 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="222" splitType="Stretch"> - <staticText> - <reportElement key="staticText-2" x="266" y="201" width="103" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="ca0f0a63-e0ef-43f8-ab71-6c40d0df52e8"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField" x="371" y="201" width="50" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="dbe10325-bdd7-4f6b-a143-aadef1d7ca2b"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-5" x="10" y="150" width="510" height="51" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="dc03ced5-39cf-4823-84c2-91a3077b45a9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Accessions and Linked Subjects, Names and Classifications" : $P{ReportHeader}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/report_config.yml b/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/report_config.yml deleted file mode 100644 index a68cb2586e..0000000000 --- a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_subjects_names_classifications_list -description: "Displays accessions and their linked names, subjects, and classifications. Report contains accession number, linked resources, accession date, title, extent, linked names, and linked subjects." diff --git a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsClassifications.jrxml b/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsClassifications.jrxml deleted file mode 100644 index 0531c22cbd..0000000000 --- a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsClassifications.jrxml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsNames.jrxml b/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsNames.jrxml deleted file mode 100644 index b2b43f7473..0000000000 --- a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsNames.jrxml +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsSubjects.jrxml b/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsSubjects.jrxml deleted file mode 100644 index d91ade4007..0000000000 --- a/reports/Accessions/AccessionsSubjectsNamesClassificationsListReport/sub_accessionsSubjects.jrxml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsUncatalogedReport/AccessionsUncatalogedReport.jrxml b/reports/Accessions/AccessionsUncatalogedReport/AccessionsUncatalogedReport.jrxml deleted file mode 100644 index ec88367aa8..0000000000 --- a/reports/Accessions/AccessionsUncatalogedReport/AccessionsUncatalogedReport.jrxml +++ /dev/null @@ -1,346 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="265" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-10" mode="Transparent" x="10" y="150" width="506" height="50" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="106ea73f-0b4f-4a84-ab5a-d9a7e4d84030"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Uncataloged Accessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-14" positionType="Float" x="392" y="227" width="49" height="15" forecolor="#000000" uuid="34b91b9f-ddaa-41c5-bf60-8fb5d3b22282"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{CATALOGED_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-13" positionType="Float" mode="Transparent" x="419" y="212" width="36" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="0f5bcfa0-aaba-4090-aef1-c89e6f884bc6"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="###0.00" isBlankWhenNull="false"> - <reportElement key="textField-9" positionType="Float" x="466" y="242" width="61" height="15" forecolor="#000000" uuid="2ad5bf0d-7713-4dcc-9104-769a7e05bf8d"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_CATALOGED_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-22" positionType="Float" mode="Transparent" x="266" y="212" width="150" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="e97e79be-a6e7-421c-b257-ed04cdb14534"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Reviewed:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-20" positionType="Float" mode="Transparent" x="266" y="242" width="195" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="ef617533-7655-45c4-8cc7-dfe859d3f981"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Uncataloged Accessions:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-18" positionType="Float" mode="Transparent" x="266" y="227" width="123" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="cb1c2b56-9c2a-484a-9167-ac6cf9d0f447"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Uncataloged Accessions:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsUncatalogedReport/report_config.yml b/reports/Accessions/AccessionsUncatalogedReport/report_config.yml deleted file mode 100644 index 3954e54ea5..0000000000 --- a/reports/Accessions/AccessionsUncatalogedReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_uncataloged -description: "Displays only those accession(s) that have not been checked as cataloged. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected that are not checked as cataloged, and the total extent number for those records not cataloged." diff --git a/reports/Accessions/AccessionsUncatalogedReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsUncatalogedReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsUncatalogedReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsUnprocessedReport/AccessionsUnprocessedReport.jrxml b/reports/Accessions/AccessionsUnprocessedReport/AccessionsUnprocessedReport.jrxml deleted file mode 100644 index d95429d181..0000000000 --- a/reports/Accessions/AccessionsUnprocessedReport/AccessionsUnprocessedReport.jrxml +++ /dev/null @@ -1,419 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="273" splitType="Stretch"> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-12" isPrintRepeatedValues="false" x="420" y="231" width="55" height="15" forecolor="#000000" uuid="370767be-2d9b-4269-b013-62a10faa839c"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MAX_DATE}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-22" mode="Transparent" x="234" y="200" width="148" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="0c969c98-534e-482d-9ba4-5345a0e0ca3e"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records Reviewed:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-21" mode="Transparent" x="234" y="215" width="136" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="39975482-7d44-47fc-89bc-d0caed2bcee6"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Unprocessed Accessioned:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-20" mode="Transparent" x="400" y="231" width="20" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="30d6e34a-6c86-4fe3-9e35-abf302b5e44a"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[&]]></text> - </staticText> - <textField evaluationTime="Report" pattern="###0.00" isBlankWhenNull="false"> - <reportElement key="textField-9" x="436" y="247" width="72" height="15" forecolor="#000000" uuid="de6dd2d5-ef70-46be-b245-263f397f0964"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_PROCESSED_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-19" mode="Transparent" x="234" y="231" width="112" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="6509429e-ff54-44bd-acbb-d699c78f33b0"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Accessioned Between:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-18" mode="Transparent" x="234" y="247" width="202" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="ed53d6e3-3b87-4830-b10a-84ad96318fce"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Unprocessed Accessions:]]></text> - </staticText> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-8" mode="Transparent" x="10" y="150" width="502" height="46" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="1f6c0613-3d90-4180-98c0-eeaf704f8fc5"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Unprocessed Accessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-14" x="371" y="215" width="79" height="15" forecolor="#000000" uuid="3a2507cc-1a9a-46d2-82ef-1e944a6c9784"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA["" + $V{PROCESSED_TESTER}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-13" mode="Transparent" x="384" y="200" width="88" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="00209b7b-db25-44fb-a980-dea977e4a77b"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" evaluationTime="Report" pattern="MM/dd/yyyy" isBlankWhenNull="true"> - <reportElement key="textField-11" isPrintRepeatedValues="false" x="345" y="231" width="55" height="15" forecolor="#000000" uuid="97f6d9f3-11fa-44a7-881d-85df6f3d02be"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Right"> - <font fontName="Arial" size="10" isBold="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{MIN_DATE}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/AccessionsUnprocessedReport/report_config.yml b/reports/Accessions/AccessionsUnprocessedReport/report_config.yml deleted file mode 100644 index ced5b45a6b..0000000000 --- a/reports/Accessions/AccessionsUnprocessedReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: accessions_unprocessed -description: "Displays only those accession(s) that have not been processed. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected with date processed, and the total extent number for those records without a completed date processed field." diff --git a/reports/Accessions/AccessionsUnprocessedReport/sub_accessionsResources.jrxml b/reports/Accessions/AccessionsUnprocessedReport/sub_accessionsResources.jrxml deleted file mode 100644 index 617004e365..0000000000 --- a/reports/Accessions/AccessionsUnprocessedReport/sub_accessionsResources.jrxml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/sub_accessionsNamesAsCreators.jrxml b/reports/Accessions/sub_accessionsNamesAsCreators.jrxml deleted file mode 100644 index a554d07e80..0000000000 --- a/reports/Accessions/sub_accessionsNamesAsCreators.jrxml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/sub_accessionsNamesAsSources.jrxml b/reports/Accessions/sub_accessionsNamesAsSources.jrxml deleted file mode 100644 index dd05921b10..0000000000 --- a/reports/Accessions/sub_accessionsNamesAsSources.jrxml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Accessions/sub_accessionsNamesAsSubjects.jrxml b/reports/Accessions/sub_accessionsNamesAsSubjects.jrxml deleted file mode 100644 index a2386dcdd0..0000000000 --- a/reports/Accessions/sub_accessionsNamesAsSubjects.jrxml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/DigitalObjects/DigitalObjectFileVersionsReport/DigitalObjectFileVersionsReport.jrxml b/reports/DigitalObjects/DigitalObjectFileVersionsReport/DigitalObjectFileVersionsReport.jrxml deleted file mode 100644 index 2326fb59d2..0000000000 --- a/reports/DigitalObjects/DigitalObjectFileVersionsReport/DigitalObjectFileVersionsReport.jrxml +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="227" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="10" y="150" width="494" height="57" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="f1f8bda7-0562-43e5-b7d2-006ecf2e984d"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Digital Object File Versions List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-14" positionType="Float" mode="Transparent" x="359" y="208" width="31" height="13" forecolor="#000000" backcolor="#FFFFFF" uuid="88513f2e-23a9-4a60-8428-35d86b2c5883"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-12" positionType="Float" mode="Transparent" x="266" y="208" width="93" height="13" forecolor="#000000" backcolor="#FFFFFF" uuid="bbef1afd-4c06-4a27-ab12-039afe116314"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/DigitalObjects/DigitalObjectFileVersionsReport/report_config.yml b/reports/DigitalObjects/DigitalObjectFileVersionsReport/report_config.yml deleted file mode 100644 index 956c8dcaab..0000000000 --- a/reports/DigitalObjects/DigitalObjectFileVersionsReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: digital_object_file_versions -description: "Displays any file versions associated with the selected digital objects." diff --git a/reports/DigitalObjects/DigitalObjectFileVersionsReport/sub_digitalObjectChildren.jrxml b/reports/DigitalObjects/DigitalObjectFileVersionsReport/sub_digitalObjectChildren.jrxml deleted file mode 100644 index c74579e346..0000000000 --- a/reports/DigitalObjects/DigitalObjectFileVersionsReport/sub_digitalObjectChildren.jrxml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/DigitalObjects/DigitalObjectFileVersionsReport/sub_digitalObjectFileVersions.jrxml b/reports/DigitalObjects/DigitalObjectFileVersionsReport/sub_digitalObjectFileVersions.jrxml deleted file mode 100644 index 1ee21bb8d1..0000000000 --- a/reports/DigitalObjects/DigitalObjectFileVersionsReport/sub_digitalObjectFileVersions.jrxml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/DigitalObjects/DigitalObjectListTableReport/DigitalObjectListTableReport.jrxml b/reports/DigitalObjects/DigitalObjectListTableReport/DigitalObjectListTableReport.jrxml deleted file mode 100644 index dd9dec0f45..0000000000 --- a/reports/DigitalObjects/DigitalObjectListTableReport/DigitalObjectListTableReport.jrxml +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="30" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="0" y="0" width="406" height="28" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="7c6f4b28-2509-490e-b741-6c46cc8fa852"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Digital Objects List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-14" positionType="Float" mode="Transparent" x="498" y="0" width="31" height="13" forecolor="#000000" backcolor="#FFFFFF" uuid="863562d7-b80d-4ca2-a0f3-620da342ec48"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-12" positionType="Float" mode="Transparent" x="406" y="0" width="92" height="13" forecolor="#000000" backcolor="#FFFFFF" uuid="c589c21d-753d-44d0-aa00-027d73136eac"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records:]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/DigitalObjects/DigitalObjectListTableReport/report_config.yml b/reports/DigitalObjects/DigitalObjectListTableReport/report_config.yml deleted file mode 100644 index deae21617a..0000000000 --- a/reports/DigitalObjects/DigitalObjectListTableReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: digital_object_list_table -description: "Displays digital object(s) in table format. Report contains title, digital object identifier, object type, dates and the title and identifier of linked resources." diff --git a/reports/Locations/LocationRecordsReport/LocationRecordsReport.jrxml b/reports/Locations/LocationRecordsReport/LocationRecordsReport.jrxml deleted file mode 100644 index 02d4b7062c..0000000000 --- a/reports/Locations/LocationRecordsReport/LocationRecordsReport.jrxml +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="256" splitType="Stretch"> - <staticText> - <reportElement key="staticText-1" positionType="Float" mode="Transparent" x="275" y="206" width="90" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="008fe4dc-f9f7-424a-a1bb-84d52e0ead6e"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Results: ]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-10" mode="Transparent" x="365" y="206" width="50" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="ecc4d837-733d-4e63-9e51-c2f3fda5d721"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-6" mode="Transparent" x="12" y="150" width="508" height="53" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="ea7cb9b3-9ba1-498c-93a2-9c8a8c67b399"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Locations with Resources and Accessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Locations/LocationRecordsReport/report_config.yml b/reports/Locations/LocationRecordsReport/report_config.yml deleted file mode 100644 index 6c43101c41..0000000000 --- a/reports/Locations/LocationRecordsReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: location_records -description: "Displays a list of locations, indicating any accessions or resources assigned to defined locations." diff --git a/reports/Locations/LocationRecordsReport/sub_locationsAccessions.jrxml b/reports/Locations/LocationRecordsReport/sub_locationsAccessions.jrxml deleted file mode 100644 index 5ebfe364f8..0000000000 --- a/reports/Locations/LocationRecordsReport/sub_locationsAccessions.jrxml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Locations/LocationRecordsReport/sub_locationsResources.jrxml b/reports/Locations/LocationRecordsReport/sub_locationsResources.jrxml deleted file mode 100644 index 5587eee10e..0000000000 --- a/reports/Locations/LocationRecordsReport/sub_locationsResources.jrxml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Names/NamesListReport/NamesListReport.jrxml b/reports/Names/NamesListReport/NamesListReport.jrxml deleted file mode 100644 index 1014cf5739..0000000000 --- a/reports/Names/NamesListReport/NamesListReport.jrxml +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="36" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-5" mode="Transparent" x="12" y="0" width="509" height="31" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="6c96fbdd-24e2-40e4-b39d-6de307d0f4a9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Name Records List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Names/NamesListReport/report_config.yml b/reports/Names/NamesListReport/report_config.yml deleted file mode 100644 index 28a3eb348c..0000000000 --- a/reports/Names/NamesListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: names_list -description: "Displays selected name record(s). Report lists name, name type, and name source." diff --git a/reports/Names/NamesToNonPrefferedReport/NamesToNonPreferredReport.jrxml b/reports/Names/NamesToNonPrefferedReport/NamesToNonPreferredReport.jrxml deleted file mode 100644 index 66f4f3b3c0..0000000000 --- a/reports/Names/NamesToNonPrefferedReport/NamesToNonPreferredReport.jrxml +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="290" splitType="Stretch"> - <staticText> - <reportElement key="staticText-1" mode="Transparent" x="266" y="237" width="90" height="18" forecolor="#000000" backcolor="#FFFFFF" uuid="e61364e7-941b-4fe9-9524-90279d0cbcfe"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of records: ]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-1" mode="Transparent" x="356" y="237" width="50" height="18" forecolor="#000000" backcolor="#FFFFFF" uuid="c64f6370-91c1-4e92-be29-7b78903c681d"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-2" mode="Transparent" x="15" y="150" width="501" height="81" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="002be56f-edc3-428a-be4a-a51b865de3db"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Name Records and Non-preferred Forms" : $P{ReportHeader}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Names/NamesToNonPrefferedReport/report_config.yml b/reports/Names/NamesToNonPrefferedReport/report_config.yml deleted file mode 100644 index d0fd1025ad..0000000000 --- a/reports/Names/NamesToNonPrefferedReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: names_to_non_preferred -description: "Displays name(s) and any associated non-preferred name(s). Report contains sort name, name type, name source and any other non-preferred forms of that name." diff --git a/reports/Names/NamesToNonPrefferedReport/sub_nonPreferredNames.jrxml b/reports/Names/NamesToNonPrefferedReport/sub_nonPreferredNames.jrxml deleted file mode 100644 index 787525f5f1..0000000000 --- a/reports/Names/NamesToNonPrefferedReport/sub_nonPreferredNames.jrxml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesDeaccessionsListReport/ResourcesDeaccessionsListReport.jrxml b/reports/Resources/ResourcesDeaccessionsListReport/ResourcesDeaccessionsListReport.jrxml deleted file mode 100644 index 0412287cb1..0000000000 --- a/reports/Resources/ResourcesDeaccessionsListReport/ResourcesDeaccessionsListReport.jrxml +++ /dev/null @@ -1,358 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="264" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="13" y="150" width="504" height="57" forecolor="#000000" backcolor="#FFFFFF" uuid="edaab38d-a30d-4421-bb0a-ef24e0e55f91"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Resources with Deaccessions" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-13" positionType="Float" x="389" y="230" width="55" height="15" forecolor="#000000" uuid="e03e0a03-ed27-48b6-81c6-1e50b1bbfdd8"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-11" positionType="Float" mode="Transparent" x="266" y="230" width="119" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="be3e210e-9c4d-4d46-b579-ee8f9de2ddaa"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Resources: ]]></text> - </staticText> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-18" positionType="Float" x="404" y="245" width="55" height="15" forecolor="#000000" uuid="cd8e8517-3798-4c0c-9105-4e6d2ea682f8"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{DEACCESSION_EXTENT_SUM} - 1]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-13" positionType="Float" mode="Transparent" x="266" y="245" width="135" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="2ae67661-4ed5-4df9-ab28-d7474bd10a3b"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Deaccessions: ]]></text> - </staticText> - <staticText> - <reportElement key="staticText-12" positionType="Float" mode="Transparent" x="266" y="215" width="94" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="300e7306-df8a-4e67-83a6-4f9cdb4d53cd"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records: ]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-14" positionType="Float" mode="Transparent" x="360" y="215" width="55" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="d072e564-bffa-478d-afea-05f12b6082d3"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesDeaccessionsListReport/report_config.yml b/reports/Resources/ResourcesDeaccessionsListReport/report_config.yml deleted file mode 100644 index c1a6dec84b..0000000000 --- a/reports/Resources/ResourcesDeaccessionsListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: resources_deaccessions_list -description: "Displays resource(s) and linked deaccession record(s). Report contains title, resource identifier, level, date range, linked deaccessions, creator names, and physical extent totals." diff --git a/reports/Resources/ResourcesDeaccessionsListReport/sub_resourcesDeaccessions.jrxml b/reports/Resources/ResourcesDeaccessionsListReport/sub_resourcesDeaccessions.jrxml deleted file mode 100644 index b7ca61bbac..0000000000 --- a/reports/Resources/ResourcesDeaccessionsListReport/sub_resourcesDeaccessions.jrxml +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesInstancesListReport/ResourcesInstancesListReport.jrxml b/reports/Resources/ResourcesInstancesListReport/ResourcesInstancesListReport.jrxml deleted file mode 100644 index 10194fcf93..0000000000 --- a/reports/Resources/ResourcesInstancesListReport/ResourcesInstancesListReport.jrxml +++ /dev/null @@ -1,314 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="247" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="14" y="150" width="507" height="54" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="4f125b1c-30b2-42f9-ab87-dd74c10d3f5f"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Resources and Instances List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-13" x="389" y="227" width="53" height="15" forecolor="#000000" uuid="e95dfa1e-0cb0-4b95-88f5-b2fed677428d"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-12" mode="Transparent" x="266" y="210" width="90" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="7e24d63d-ef84-440b-99b1-e09e45b234e9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records: ]]></text> - </staticText> - <staticText> - <reportElement key="staticText-11" mode="Transparent" x="266" y="227" width="119" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="0194ceb8-2bda-42c5-a33d-35e5b709ca90"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Resources:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-14" mode="Transparent" x="360" y="210" width="53" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="20a9a7a8-935e-4729-b34a-4faabc9a2730"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesInstancesListReport/report_config.yml b/reports/Resources/ResourcesInstancesListReport/report_config.yml deleted file mode 100644 index 18c6554fa8..0000000000 --- a/reports/Resources/ResourcesInstancesListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: resources_instances_list -description: "Displays resource(s) and all specified location information. Report contains title, resource identifier, level, date range, and assigned locations." diff --git a/reports/Resources/ResourcesInstancesListReport/sub_resourcesInstances.jrxml b/reports/Resources/ResourcesInstancesListReport/sub_resourcesInstances.jrxml deleted file mode 100644 index 7db2adaaef..0000000000 --- a/reports/Resources/ResourcesInstancesListReport/sub_resourcesInstances.jrxml +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesLocationsListReport/ResourcesLocationsListReport.jrxml b/reports/Resources/ResourcesLocationsListReport/ResourcesLocationsListReport.jrxml deleted file mode 100644 index 087df9c129..0000000000 --- a/reports/Resources/ResourcesLocationsListReport/ResourcesLocationsListReport.jrxml +++ /dev/null @@ -1,314 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="247" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="14" y="150" width="507" height="54" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="4f125b1c-30b2-42f9-ab87-dd74c10d3f5f"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Resources and Locations List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField-13" x="389" y="227" width="53" height="15" forecolor="#000000" uuid="e95dfa1e-0cb0-4b95-88f5-b2fed677428d"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_SUM}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-12" mode="Transparent" x="266" y="210" width="90" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="7e24d63d-ef84-440b-99b1-e09e45b234e9"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records: ]]></text> - </staticText> - <staticText> - <reportElement key="staticText-11" mode="Transparent" x="266" y="227" width="119" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="0194ceb8-2bda-42c5-a33d-35e5b709ca90"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Resources:]]></text> - </staticText> - <textField evaluationTime="Report" pattern="" isBlankWhenNull="false"> - <reportElement key="textField-14" mode="Transparent" x="360" y="210" width="53" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="20a9a7a8-935e-4729-b34a-4faabc9a2730"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$V{REPORT_COUNT}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesLocationsListReport/report_config.yml b/reports/Resources/ResourcesLocationsListReport/report_config.yml deleted file mode 100644 index f309bc9a7c..0000000000 --- a/reports/Resources/ResourcesLocationsListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: resources_locations_list -description: "Displays resource(s) and all specified location information. Report contains title, resource identifier, level, date range, and assigned locations." diff --git a/reports/Resources/ResourcesLocationsListReport/sub_resourcesLocations.jrxml b/reports/Resources/ResourcesLocationsListReport/sub_resourcesLocations.jrxml deleted file mode 100644 index fa6170d28a..0000000000 --- a/reports/Resources/ResourcesLocationsListReport/sub_resourcesLocations.jrxml +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesRestrictionsListReport/ResourcesRestrictionsListReport.jrxml b/reports/Resources/ResourcesRestrictionsListReport/ResourcesRestrictionsListReport.jrxml deleted file mode 100644 index 10c6edfaf9..0000000000 --- a/reports/Resources/ResourcesRestrictionsListReport/ResourcesRestrictionsListReport.jrxml +++ /dev/null @@ -1,364 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="240" splitType="Stretch"> - <textField evaluationTime="Report" isBlankWhenNull="false"> - <reportElement key="textField" x="361" y="202" width="57" height="15" forecolor="#000000" uuid="0329ee9e-b5e0-4041-b21a-07ab952b5de3"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="10" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{TEST_RESTRICTIONS_COUNT}]]></textFieldExpression> - </textField> - <textField evaluationTime="Report" pattern="#,##0.00" isBlankWhenNull="false"> - <reportElement key="textField-18" x="391" y="221" width="57" height="13" forecolor="#000000" uuid="8962edd1-5cc4-490e-80fa-79a79da6f438"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement> - <font fontName="Arial" size="8" isBold="false" isItalic="false" pdfFontName="Helvetica"/> - </textElement> - <textFieldExpression><![CDATA[$V{EXTENT_RESTRICTIONS_SUM}]]></textFieldExpression> - </textField> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-12" mode="Transparent" x="14" y="150" width="501" height="52" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="eea033a4-4f74-494e-8c7e-d6090314c7c5"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Restricted Resources" : $P{ReportHeader}]]></textFieldExpression> - </textField> - <staticText> - <reportElement key="staticText-11" mode="Transparent" x="266" y="219" width="119" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="1a8bf3d3-3f9a-4ff1-af34-611fb8fcecf7"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Total Extent of Resources:]]></text> - </staticText> - <staticText> - <reportElement key="staticText-12" mode="Transparent" x="266" y="202" width="90" height="15" forecolor="#000000" backcolor="#FFFFFF" uuid="ad1ffcbd-f929-4c52-b54e-3b3f5d7c3a95"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Left" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="10" isBold="false" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <text><![CDATA[Number of Records: ]]></text> - </staticText> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Resources/ResourcesRestrictionsListReport/report_config.yml b/reports/Resources/ResourcesRestrictionsListReport/report_config.yml deleted file mode 100644 index 3b1a07bd6b..0000000000 --- a/reports/Resources/ResourcesRestrictionsListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: resources_restrictions_list -description: "Displays only those resource(s) that are restricted. Report contains title, resource identifier, level, date range, creator names, and a total extent number of the records selected that are checked as restrictions apply." diff --git a/reports/Resources/ResourcesRestrictionsListReport/sub_resourcesNamesAsCreators.jrxml b/reports/Resources/ResourcesRestrictionsListReport/sub_resourcesNamesAsCreators.jrxml deleted file mode 100644 index 51e1942a8e..0000000000 --- a/reports/Resources/ResourcesRestrictionsListReport/sub_resourcesNamesAsCreators.jrxml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band splitType="Stretch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Subjects/SubjectListReport/SubjectListReport.jrxml b/reports/Subjects/SubjectListReport/SubjectListReport.jrxml deleted file mode 100644 index 2aa42ae7b0..0000000000 --- a/reports/Subjects/SubjectListReport/SubjectListReport.jrxml +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - <band height="157" splitType="Stretch"> - <textField isStretchWithOverflow="true" pattern="" isBlankWhenNull="true"> - <reportElement key="textField-5" mode="Transparent" x="14" y="90" width="509" height="60" isPrintWhenDetailOverflows="true" forecolor="#000000" backcolor="#FFFFFF" uuid="7b940dac-6e66-43e3-9652-067a0ed9b221"/> - <box> - <topPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <leftPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - <bottomPen lineWidth="0.0" lineColor="#000000"/> - <rightPen lineWidth="0.0" lineStyle="Solid" lineColor="#000000"/> - </box> - <textElement textAlignment="Center" verticalAlignment="Top" rotation="None"> - <font fontName="Arial" size="20" isBold="true" isItalic="false" isUnderline="false" isStrikeThrough="false" pdfFontName="Helvetica-Bold" pdfEncoding="Cp1252" isPdfEmbedded="false"/> - <paragraph lineSpacing="Single"/> - </textElement> - <textFieldExpression><![CDATA[$P{ReportHeader}.equals("") ? "Subject Records List" : $P{ReportHeader}]]></textFieldExpression> - </textField> - </band> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reports/Subjects/SubjectListReport/report_config.yml b/reports/Subjects/SubjectListReport/report_config.yml deleted file mode 100644 index b6874c261a..0000000000 --- a/reports/Subjects/SubjectListReport/report_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -report_type: jdbc -uri_suffix: subject_list -description: "Displays selected subject record(s). Report lists subject term, subject term type, and subject source." diff --git a/reports/accessions/accession_cataloged_report/accession_cataloged_report.erb b/reports/accessions/accession_cataloged_report/accession_cataloged_report.erb new file mode 100644 index 0000000000..4bedcd7d7d --- /dev/null +++ b/reports/accessions/accession_cataloged_report/accession_cataloged_report.erb @@ -0,0 +1,41 @@ +
      +
      <%= @report.title %>
      + +
      +
      <%= t('number_of_records') %>
      +
      <%= @report.total_count %>
      +
      <%= t('number_of_cataloged') %>
      +
      <%= @report.cataloged_count %>
      +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      +
      + +
      + +<% @report.each do |accession| %> + <% next if accession.fetch('cataloged') == 0 %> + +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      <%= t('date_processed') %>
      +
      <%= format_date(accession.fetch('accessionProcessedDate')) %>
      +
      <%= t('cataloged') %>
      +
      + <% if accession.fetch('catalogedDate').nil? || accession.fetch('catalogedDate').empty? %> + <%= t("cataloged_false") %> + <% else %> + <%= format_date(accession.fetch('catalogedDate')) %> + <% end %> +
      +
      + + <%= subreport_section(t('linked_resources'), AccessionResourcesSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_cataloged_report/accession_cataloged_report.rb b/reports/accessions/accession_cataloged_report/accession_cataloged_report.rb new file mode 100644 index 0000000000..61cd34e1e8 --- /dev/null +++ b/reports/accessions/accession_cataloged_report/accession_cataloged_report.rb @@ -0,0 +1,41 @@ +class AccessionCatalogedReport < AbstractReport + + register_report + + def template + 'accession_cataloged_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionCatalogedDate(id)'), :catalogedDate), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + + # TODO: subreport for linked resources + + # Number of Records Reviewed + def total_count + @totalCount ||= self.query.count + end + + # Cataloged Accessions + def cataloged_count + @catalogedCount ||= db.from(self.query).where(:cataloged => 1).count + end + + # Total Extent of Cataloged Accessions + def total_extent + @totalExtent ||= db.from(self.query).where(:cataloged => 1).sum(:extentNumber) + end +end diff --git a/reports/accessions/accession_cataloged_report/en.yml b/reports/accessions/accession_cataloged_report/en.yml new file mode 100644 index 0000000000..45a2583ee5 --- /dev/null +++ b/reports/accessions/accession_cataloged_report/en.yml @@ -0,0 +1,14 @@ +en: + reports: + accession_cataloged_report: + title: Cataloged Accessions + description: Displays only those accessions that have been cataloged. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected that are checked as cataloged, and the total extent number for those records cataloged. + number_of_records: Number of Records Reviewed + number_of_cataloged: Cataloged Accessions + total_extent: Total Extent of Cataloged Accessions + identifier_prefix: Accession + extent: Extent + date_processed: Date Processed + cataloged: Cataloged + cataloged_false: [Date not recorded] + linked_resources: Linked Resource(s) \ No newline at end of file diff --git a/reports/accessions/accession_classifications_subreport/accession_classifications_subreport.erb b/reports/accessions/accession_classifications_subreport/accession_classifications_subreport.erb new file mode 100644 index 0000000000..8f10ede02b --- /dev/null +++ b/reports/accessions/accession_classifications_subreport/accession_classifications_subreport.erb @@ -0,0 +1,11 @@ + + + + + <% @report.each do |classification| %> + + + + + <% end %> +
      <%= t('identifier') %><%= t('title') %>
      <%= h classification['classificationIdentifier'] || classification['classificationTermIdentifier'] %><%= h classification['classificationTitle'] || classification['classificationTermTitle'] %>
      diff --git a/reports/accessions/accession_classifications_subreport/accession_classifications_subreport.rb b/reports/accessions/accession_classifications_subreport/accession_classifications_subreport.rb new file mode 100644 index 0000000000..a4755cd0b0 --- /dev/null +++ b/reports/accessions/accession_classifications_subreport/accession_classifications_subreport.rb @@ -0,0 +1,18 @@ +class AccessionClassificationsSubreport < AbstractReport + + def template + "accession_classifications_subreport.erb" + end + + def query + db[:classification_rlshp] + .left_outer_join(:classification, :classification__id => :classification_rlshp__classification_id) + .left_outer_join(:classification_term, :classification_term__id => :classification_rlshp__classification_term_id) + .filter(:accession_id => @params.fetch(:accessionId)) + .select(Sequel.as(:classification__identifier, :classificationIdentifier), + Sequel.as(:classification__title, :classificationTitle), + Sequel.as(:classification_term__identifier, :classificationTermIdentifier), + Sequel.as(:classification_term__title, :classificationTermTitle)) + end + +end diff --git a/reports/accessions/accession_classifications_subreport/en.yml b/reports/accessions/accession_classifications_subreport/en.yml new file mode 100644 index 0000000000..40e8db907e --- /dev/null +++ b/reports/accessions/accession_classifications_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + accession_classifications_subreport: + identifier: Identifier + title: Title \ No newline at end of file diff --git a/reports/accessions/accession_deaccessions_list_report/accession_deaccessions_list_report.erb b/reports/accessions/accession_deaccessions_list_report/accession_deaccessions_list_report.erb new file mode 100644 index 0000000000..58f007bef7 --- /dev/null +++ b/reports/accessions/accession_deaccessions_list_report/accession_deaccessions_list_report.erb @@ -0,0 +1,32 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('number_of_records') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('accessioned_between') %>
      +
      <%= format_date(@report.from_date) %> & <%= format_date(@report.to_date) %>
      +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      +
      <%= t('total_deaccessions_extent') %>
      +
      <%= format_number(@report.total_extent_of_deaccessions) %>
      +
      +
      + +<% @report.each do |accession| %> +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      +
      <%= t('accession_date') %>
      +
      <%= format_date(accession.fetch('accessionDate')) %>
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      + + <%= subreport_section(t('deaccessions'), AccessionDeaccessionsSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_deaccessions_list_report/accession_deaccessions_list_report.rb b/reports/accessions/accession_deaccessions_list_report/accession_deaccessions_list_report.rb new file mode 100644 index 0000000000..d240b2d250 --- /dev/null +++ b/reports/accessions/accession_deaccessions_list_report/accession_deaccessions_list_report.rb @@ -0,0 +1,53 @@ +class AccessionDeaccessionsListReport < AbstractReport + + register_report + + def template + 'accession_deaccessions_list_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + # Number of Records + def total_count + @total_count ||= self.query.count + end + + # Accessioned Between - From Date + def from_date + @from_date ||= self.query.min(:accession_date) + end + + # Accessioned Between - To Date + def to_date + @to_date ||= self.query.max(:accession_date) + end + + # Total Extent of Accessions + def total_extent + @total_extent ||= db.from(self.query).sum(:extentNumber) + end + + # Total Extent of Deaccessions + def total_extent_of_deaccessions + return @total_extent_of_deaccessions if @total_extent_of_deaccessions + + deaccessions = db[:deaccession].where(:accession_id => self.query.select(:id)) + deaccession_extents = db[:extent].where(:deaccession_id => deaccessions.select(:id)) + + @total_extent_of_deaccessions = deaccession_extents.sum(:number) + + @total_extent_of_deaccessions + end + +end diff --git a/reports/accessions/accession_deaccessions_list_report/en.yml b/reports/accessions/accession_deaccessions_list_report/en.yml new file mode 100644 index 0000000000..31343436c0 --- /dev/null +++ b/reports/accessions/accession_deaccessions_list_report/en.yml @@ -0,0 +1,13 @@ +en: + reports: + accession_deaccessions_list_report: + title: Accessions Acquired and Linked Deaccession Records + description: Displays a list of accession record(s) and linked deaccession record(s). Report contains accession number, title, extent, accession date, container summary, cataloged, date processed, rights transferred, linked deaccessions and total extent of all deaccessions. + number_of_records: Number of Records + accessioned_between: Accessioned Between + total_extent: Total Extent of Accessions + total_deaccessions_extent: Total Extent of Deaccessions + identifier_prefix: Accession + accession_date: Accession Date + extent: Extent + deaccessions: Deaccessions \ No newline at end of file diff --git a/reports/accessions/accession_deaccessions_subreport/accession_deaccessions_subreport.erb b/reports/accessions/accession_deaccessions_subreport/accession_deaccessions_subreport.erb new file mode 100644 index 0000000000..425d5d5937 --- /dev/null +++ b/reports/accessions/accession_deaccessions_subreport/accession_deaccessions_subreport.erb @@ -0,0 +1,15 @@ + + + + + + + <% @report.each do |deaccession| %> + + + + + + + <% end %> +
      <%= t('description') %><%= t('extent') %><%= t('date') %><%= t('notification_sent') %>
      <%= h deaccession.fetch('description') %><%= format_number(deaccession.fetch('extentNumber')) %> <%= deaccession.fetch('extentType') %><%= format_date(deaccession.fetch('deaccessionDate')) %><%= format_boolean(deaccession.fetch('notification')) %>
      diff --git a/reports/accessions/accession_deaccessions_subreport/accession_deaccessions_subreport.rb b/reports/accessions/accession_deaccessions_subreport/accession_deaccessions_subreport.rb new file mode 100644 index 0000000000..bb444f77d3 --- /dev/null +++ b/reports/accessions/accession_deaccessions_subreport/accession_deaccessions_subreport.rb @@ -0,0 +1,22 @@ +class AccessionDeaccessionsSubreport < AbstractReport + + def template + "accession_deaccessions_subreport.erb" + end + + def accession_count + query.count + end + + def query + db[:deaccession] + .filter(:accession_id => @params.fetch(:accessionId)) + .select(Sequel.as(:id, :deaccessionId), + Sequel.as(:description, :description), + Sequel.as(:notification, :notification), + Sequel.as(Sequel.lit("GetDeaccessionDate(id)"), :deaccessionDate), + Sequel.as(Sequel.lit("GetDeaccessionExtent(id)"), :extentNumber), + Sequel.as(Sequel.lit("GetDeaccessionExtentType(id)"), :extentType)) + end + +end diff --git a/reports/accessions/accession_deaccessions_subreport/en.yml b/reports/accessions/accession_deaccessions_subreport/en.yml new file mode 100644 index 0000000000..837374f4a4 --- /dev/null +++ b/reports/accessions/accession_deaccessions_subreport/en.yml @@ -0,0 +1,7 @@ +en: + reports: + accession_deaccessions_subreport: + description: Description + extent: Extent + date: Date + notification_sent: Notification Sent \ No newline at end of file diff --git a/reports/accessions/accession_inventory_report/accession_inventory_report.erb b/reports/accessions/accession_inventory_report/accession_inventory_report.erb new file mode 100644 index 0000000000..90a3bf2a1a --- /dev/null +++ b/reports/accessions/accession_inventory_report/accession_inventory_report.erb @@ -0,0 +1,30 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('number_of_records') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('number_with_inventories') %>
      +
      <%= h @report.total_with_inventories %>
      +
      +
      + +<% @report.each do |accession| %> + <% next unless accession['inventory'] %> + +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      + + <%= text_section(t('inventory'), accession.fetch('inventory')) %> + + <%= subreport_section(t('linked_resources'), AccessionResourcesSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_inventory_report/accession_inventory_report.rb b/reports/accessions/accession_inventory_report/accession_inventory_report.rb new file mode 100644 index 0000000000..dc4d60f4e6 --- /dev/null +++ b/reports/accessions/accession_inventory_report/accession_inventory_report.rb @@ -0,0 +1,37 @@ +class AccessionInventoryReport < AbstractReport + + register_report + + def template + 'accession_inventory_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(:inventory, :inventory), + Sequel.as(Sequel.lit('GetAccessionDatePart(id, \'inclusive\', 0)'), :dateExpression), + Sequel.as(Sequel.lit('GetAccessionDatePart(id, \'inclusive\', 1)'), :dateBegin), + Sequel.as(Sequel.lit('GetAccessionDatePart(id, \'inclusive\', 2)'), :dateEnd), + Sequel.as(Sequel.lit('GetAccessionDatePart(id, \'bulk\', 1)'), :bulkDateBegin), + Sequel.as(Sequel.lit('GetAccessionDatePart(id, \'bulk\', 2)'), :bulkDateEnd), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + # Number of Records Reviewed + def total_count + @total_count ||= self.query.count + end + + # Accessions with Inventories + def total_with_inventories + @total_with_inventories ||= self.query.where(Sequel.~(:inventory => nil)).count + end + +end diff --git a/reports/accessions/accession_inventory_report/en.yml b/reports/accessions/accession_inventory_report/en.yml new file mode 100644 index 0000000000..107ca94add --- /dev/null +++ b/reports/accessions/accession_inventory_report/en.yml @@ -0,0 +1,11 @@ +en: + reports: + accession_inventory_report: + title: Accessions with Inventories + description: Displays only those accession records with an inventory. Report contains accession number, linked resources, title, extent, accession date, container summary, and inventory. + number_of_records: Number of Records Reviewed + number_with_inventories: Accessions with Inventories + identifier_prefix: Accession + extent: Extent + inventory: Inventory + linked_resources: Linked Resource(s) \ No newline at end of file diff --git a/reports/accessions/accession_locations_subreport/accession_locations_subreport.erb b/reports/accessions/accession_locations_subreport/accession_locations_subreport.erb new file mode 100644 index 0000000000..62ed84e621 --- /dev/null +++ b/reports/accessions/accession_locations_subreport/accession_locations_subreport.erb @@ -0,0 +1,13 @@ + + + + + + + <% @report.each do |location| %> + + + + + <% end %> +
      <%= t('location') %><%= t('container') %>
      <%= h location.fetch('location') %><%= h location.fetch('container') %>
      diff --git a/reports/accessions/accession_locations_subreport/accession_locations_subreport.rb b/reports/accessions/accession_locations_subreport/accession_locations_subreport.rb new file mode 100644 index 0000000000..c93da8eddd --- /dev/null +++ b/reports/accessions/accession_locations_subreport/accession_locations_subreport.rb @@ -0,0 +1,22 @@ +class AccessionLocationsSubreport < AbstractReport + + def template + "accession_locations_subreport.erb" + end + + def query + db[:instance] + .inner_join(:sub_container, :instance_id => :instance__id) + .inner_join(:top_container_link_rlshp, :sub_container_id => :sub_container__id) + .inner_join(:top_container, :id => :top_container_link_rlshp__top_container_id) + .left_outer_join(:top_container_profile_rlshp, :top_container_id => :top_container__id) + .left_outer_join(:container_profile, :id => :top_container_profile_rlshp__container_profile_id) + .inner_join(:top_container_housed_at_rlshp, :top_container_id => :top_container__id) + .inner_join(:location, :id => :top_container_housed_at_rlshp__location_id) + .group_by(:location__id) + .filter(:instance__accession_id => @params.fetch(:accessionId)) + .select(Sequel.as(:location__title, :location), + Sequel.as(Sequel.lit("GROUP_CONCAT(CONCAT(COALESCE(container_profile.name, ''), ' ', top_container.indicator) SEPARATOR ', ')"), :container)) + end + +end diff --git a/reports/accessions/accession_locations_subreport/en.yml b/reports/accessions/accession_locations_subreport/en.yml new file mode 100644 index 0000000000..cbf20fd501 --- /dev/null +++ b/reports/accessions/accession_locations_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + accession_locations_subreport: + location: Location + container: Container \ No newline at end of file diff --git a/reports/accessions/accession_names_subreport/accession_names_subreport.erb b/reports/accessions/accession_names_subreport/accession_names_subreport.erb new file mode 100644 index 0000000000..7045654c05 --- /dev/null +++ b/reports/accessions/accession_names_subreport/accession_names_subreport.erb @@ -0,0 +1,13 @@ + + + <%= t('name') %> + <%= t('function') %> + <%= t('role') %> + <% @report.each do |name| %> + + + + + + <% end %> +
      <%= h name.fetch('sortName') %><%= h name.fetch('nameLinkFunction') %><%= h name.fetch('role') %>
      diff --git a/reports/accessions/accession_names_subreport/accession_names_subreport.rb b/reports/accessions/accession_names_subreport/accession_names_subreport.rb new file mode 100644 index 0000000000..79ca5932ea --- /dev/null +++ b/reports/accessions/accession_names_subreport/accession_names_subreport.rb @@ -0,0 +1,16 @@ +class AccessionNamesSubreport < AbstractReport + + def template + "accession_names_subreport.erb" + end + + def query + db[:linked_agents_rlshp] + .filter(:accession_id => @params.fetch(:accessionId)) + .select(Sequel.as(:id, :linked_agents_rlshp_id), + Sequel.as(Sequel.lit("GetAgentSortname(agent_person_id, agent_family_id, agent_corporate_entity_id)"), :sortName), + Sequel.as(Sequel.lit("GetEnumValueUF(role_id)"), :nameLinkFunction), + Sequel.as(Sequel.lit("GetEnumValue(relator_id)"), :role)) + end + +end diff --git a/reports/accessions/accession_names_subreport/en.yml b/reports/accessions/accession_names_subreport/en.yml new file mode 100644 index 0000000000..7ce94baead --- /dev/null +++ b/reports/accessions/accession_names_subreport/en.yml @@ -0,0 +1,6 @@ +en: + reports: + accession_names_subreport: + name: Name + function: Function + role: Role \ No newline at end of file diff --git a/reports/accessions/accession_processed_report/accession_processed_report.erb b/reports/accessions/accession_processed_report/accession_processed_report.erb new file mode 100644 index 0000000000..9c292f9b8a --- /dev/null +++ b/reports/accessions/accession_processed_report/accession_processed_report.erb @@ -0,0 +1,36 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('number_of_records') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('processed_accessions') %>
      +
      <%= h @report.total_processed %>
      +
      <%= t('accessioned_between') %>
      +
      <%= format_date(@report.from_date) %> &amp; <%= format_date(@report.to_date) %>
      +
      <%= t('total_extent_of_processed') %>
      +
      <%= format_number(@report.total_extent_of_processed) %>
      +
      +
      + +<% @report.each do |accession| %> + <% next unless accession.fetch('accessionProcessed') == '1' %> + +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      +
      <%= t('date_processed') %>
      +
      <%= format_date(accession.fetch('accessionProcessedDate')) %>
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      <%= t('cataloged') %>
      +
      <%= format_boolean(accession.fetch('cataloged') == 1) %>
      +
      + + <%= subreport_section(t('linked_resources'), AccessionResourcesSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_processed_report/accession_processed_report.rb b/reports/accessions/accession_processed_report/accession_processed_report.rb new file mode 100644 index 0000000000..1c724ac838 --- /dev/null +++ b/reports/accessions/accession_processed_report/accession_processed_report.rb @@ -0,0 +1,48 @@ +class AccessionProcessedReport < AbstractReport + + register_report + + def template + 'accession_processed_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + # Number of Records Reviewed + def total_count + @total_count ||= self.query.count + end + + # Processed Accessions + def total_processed + @total_processed ||= db.from(self.query).where(:accessionProcessed => 1).count + end + + # Accessioned Between - From Date + def from_date + @from_date ||= self.query.min(:accession_date) + end + + # Accessioned Between - To Date + def to_date + @to_date ||= self.query.max(:accession_date) + end + + #Total Extent of Processed Accessions + def total_extent_of_processed + @total_extent_of_processed ||= db.from(self.query).where(:accessionProcessed => 1).sum(:extentNumber) + end +end diff --git a/reports/accessions/accession_processed_report/en.yml b/reports/accessions/accession_processed_report/en.yml new file mode 100644 index 0000000000..3d96e31a3a --- /dev/null +++ b/reports/accessions/accession_processed_report/en.yml @@ -0,0 +1,14 @@ +en: + reports: + accession_processed_report: + title: Processed Accessions + description: Displays only those accession(s) that have been processed based on the date processed field. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected with a date processed, and the total extent number for those records with date processed. + number_of_records: Number of Records + processed_accessions: Processed Accessions + accessioned_between: Accessioned Between + total_extent_of_processed: Total Extent of Processed Accessions + identifier_prefix: Accession + extent: Extent + date_processed: Date Processed + cataloged: Cataloged + linked_resources: Linked Resource(s) \ No newline at end of file diff --git a/reports/accessions/accession_production_report/accession_production_report.erb b/reports/accessions/accession_production_report/accession_production_report.erb new file mode 100644 index 0000000000..aedccab69f --- /dev/null +++ b/reports/accessions/accession_production_report/accession_production_report.erb @@ -0,0 +1,21 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('accessioned_between') %>
      +
      <%= format_date(@report.from_date) %> & <%= format_date(@report.to_date) %>
      + +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      + +
      <%= t('number_selected') %>
      +
      <%= h @report.total_count %>
      + +
      <%= t('total_cataloged_extent') %>
      +
      <%= format_number(@report.total_extent_of_cataloged) %>
      + +
      <%= t('total_processed_extent') %>
      +
      <%= format_number(@report.total_extent_of_processed) %>
      +
      + +
      diff --git a/reports/accessions/accession_production_report/accession_production_report.rb b/reports/accessions/accession_production_report/accession_production_report.rb new file mode 100644 index 0000000000..ccb7aed32b --- /dev/null +++ b/reports/accessions/accession_production_report/accession_production_report.rb @@ -0,0 +1,57 @@ +class AccessionProductionReport < AbstractReport + + register_report + + def template + 'accession_production_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + # Accessioned Between - From Date + def from_date + @from_date ||= self.query.min(:accession_date) + end + + # Accessioned Between - To Date + def to_date + @to_date ||= self.query.max(:accession_date) + end + + # Total Extent of Selected Accessions + def total_extent + @total_extent ||= db.from(self.query).sum(:extentNumber) + end + + # Number of Records Selected + def total_count + @total_count ||= self.query.count + end + + # Total Extent of Cataloged Accessions + def total_extent_of_cataloged + @total_extent_of_cataloged ||= db.from(self.query). + filter(:cataloged => 1). + sum(:extentNumber) + end + + # Total Extent of Cataloged Accessions + def total_extent_of_processed + @total_extent_of_processed ||= db.from(self.query). + filter(:accessionProcessed => 1). + sum(:extentNumber) + end + +end diff --git a/reports/accessions/accession_production_report/en.yml b/reports/accessions/accession_production_report/en.yml new file mode 100644 index 0000000000..d2f1c5f6eb --- /dev/null +++ b/reports/accessions/accession_production_report/en.yml @@ -0,0 +1,10 @@ +en: + reports: + accession_production_report: + title: Accession Production Report + description: Displays accessions that have been accessioned, processed, and cataloged during a specified time period. Produces a summary statement of the total number of accessions, the total extent, total extent processed, and extent cataloged within the specified date range. + accessioned_between: Accessioned Between + total_extent: Total Extent of Selected Accessions + number_selected: Number of Records Selected + total_cataloged_extent: Total Extent of Cataloged Accessions + total_processed_extent: Total Extent of Processed Accessions \ No newline at end of file diff --git a/reports/accessions/accession_receipt_report/accession_receipt_report.erb b/reports/accessions/accession_receipt_report/accession_receipt_report.erb new file mode 100644 index 0000000000..1680473191 --- /dev/null +++ b/reports/accessions/accession_receipt_report/accession_receipt_report.erb @@ -0,0 +1,26 @@ +<% @report.each do |accession| %> +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('accession_title') %>
      +
      <%= h accession.fetch('title') %>
      + + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> + +
      <%= t('repository') %>
      +
      <%= h accession.fetch('repositoryName') %>
      + + +
      <%= t('repository_date') %>
      +
      <%= format_date(accession.fetch('accessionDate')) %>
      + +
      <%= t('accession_number') %>
      +
      <%= format_4part(accession.fetch('accessionNumber')) %>
      + +
      +
      +<% end %> diff --git a/reports/accessions/accession_receipt_report/accession_receipt_report.rb b/reports/accessions/accession_receipt_report/accession_receipt_report.rb new file mode 100644 index 0000000000..7beadea23e --- /dev/null +++ b/reports/accessions/accession_receipt_report/accession_receipt_report.rb @@ -0,0 +1,22 @@ +class AccessionReceiptReport < AbstractReport + + register_report + + def template + 'accession_receipt_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetRepositoryName(repo_id)'), :repositoryName), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + +end diff --git a/reports/accessions/accession_receipt_report/en.yml b/reports/accessions/accession_receipt_report/en.yml new file mode 100644 index 0000000000..7c21dc1df3 --- /dev/null +++ b/reports/accessions/accession_receipt_report/en.yml @@ -0,0 +1,10 @@ +en: + reports: + accession_receipt_report: + title: Accession Receipt Report + description: Displays a receipt indicating accessioning of materials. Report contains accession number, title, extent, accession date, and repository. + accession_title: Accession title + extent: Extent + repository: Repository + repository_date: Added to repository on + accession_number: Accession number \ No newline at end of file diff --git a/reports/accessions/accession_report/accession_report.erb b/reports/accessions/accession_report/accession_report.erb new file mode 100644 index 0000000000..4e8e2688b9 --- /dev/null +++ b/reports/accessions/accession_report/accession_report.erb @@ -0,0 +1,75 @@ +
      + +
      <%= @report.title %>
      + +
      +
      <%= t('number_of_accessions') %>
      +
      <%= h @report.accession_count %>
      +
      +
      + +<% @report.each do |accession| %> + +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      +
      <%= t('accession_date') %>
      +
      <%= format_date(accession.fetch('accessionDate')) %>
      + +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      +
      + + <%= text_section(t('container'), accession.fetch('containerSummary')) %> + + <%= subreport_section(t('deaccessions'), AccessionDeaccessionsSubreport, accession) %> + + <%= subreport_section(t('locations'), AccessionLocationsSubreport, accession) %> + + <%= text_section(t('general_note'), accession.fetch('containerSummary')) %> + +
      +
      <%= t('begin_date') %>
      +
      <%= format_date(accession.fetch('dateBegin')) %>
      +
      <%= t('end_date') %>
      +
      <%= format_date(accession.fetch('dateEnd')) %>
      +
      <%= t('bulk_begin_date') %>
      +
      <%= format_date(accession.fetch('bulkDateBegin')) %>
      +
      <%= t('bulk_end_date') %>
      +
      <%= format_date(accession.fetch('bulkDateEnd')) %>
      +
      <%= t('date_expression') %>
      +
      <%= h accession.fetch('dateExpression') %>
      +
      + + <%= subreport_section(t('names'), AccessionNamesSubreport, accession) %> + + <%= subreport_section(t('subjects'), AccessionSubjectsSubreport, accession) %> + +
      +
      <%= t('acquisitions') %>
      +
      <%= h accession.fetch('acquisitionType') %>
      +
      + + <%= text_section(t('retention'), accession.fetch('retentionRule')) %> + + <%= text_section(t('description'), accession.fetch('descriptionNote')) %> + + <%= text_section(t('condition'), accession.fetch('conditionNote')) %> + + <%= text_section(t('inventory'), accession.fetch('inventory')) %> + + <%= text_section(t('disposition'), accession.fetch('dispositionNote')) %> + + <%= text_section(t('restrictions_apply'), format_boolean(accession.fetch('restrictionsApply'))) %> + + <%= text_section(t('access_restrictions_note'), accession.fetch('accessRestrictionsNote')) %> + + <%= text_section(t('user_restrictions_note'), accession.fetch('useRestrictionsNote')) %> + + <%= text_section(t('rights_transferred'), format_boolean(accession.fetch('rightsTransferred'))) %> + + <%= text_section(t('rights_transferred_note'), format_boolean(accession.fetch('rightsTransferredNote'))) %> +
      +<% end %> diff --git a/reports/accessions/accession_report/accession_report.rb b/reports/accessions/accession_report/accession_report.rb new file mode 100644 index 0000000000..f65123bdd7 --- /dev/null +++ b/reports/accessions/accession_report/accession_report.rb @@ -0,0 +1,59 @@ +class AccessionReport < AbstractReport + + register_report + + def scope_by_repo_id(dataset) + # repo scope is applied in the query below + dataset + end + + def template + "accession_report.erb" + end + + def headers + query.columns.map(&:to_s) + end + + def processor + { + } + end + + def accession_count + query.count + end + + def query + db[:accession] + .select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit("GetAccessionExtent(id)"), :extentNumber), + Sequel.as(Sequel.lit("GetAccessionExtentType(id)"), :extentType), + Sequel.as(:general_note, :generalNote), + Sequel.as(Sequel.lit("GetAccessionContainerSummary(id)"), :containerSummary), + Sequel.as(Sequel.lit("GetAccessionDatePart(id, 'inclusive', 0)"), :dateExpression), + Sequel.as(Sequel.lit("GetAccessionDatePart(id, 'inclusive', 1)"), :dateBegin), + Sequel.as(Sequel.lit("GetAccessionDatePart(id, 'inclusive', 2)"), :dateEnd), + Sequel.as(Sequel.lit("GetAccessionDatePart(id, 'bulk', 1)"), :bulkDateBegin), + Sequel.as(Sequel.lit("GetAccessionDatePart(id, 'bulk', 2)"), :bulkDateEnd), + Sequel.as(Sequel.lit("GetEnumValueUF(acquisition_type_id)"), :acquisitionType), + Sequel.as(:retention_rule, :retentionRule), + Sequel.as(:content_description, :descriptionNote), + Sequel.as(:condition_description, :conditionNote), + Sequel.as(:inventory, :inventory), + Sequel.as(:disposition, :dispositionNote), + Sequel.as(:restrictions_apply, :restrictionsApply), + Sequel.as(:access_restrictions, :accessRestrictions), + Sequel.as(:access_restrictions_note, :accessRestrictionsNote), + Sequel.as(:use_restrictions, :useRestrictions), + Sequel.as(:use_restrictions_note, :useRestrictionsNote), + Sequel.as(Sequel.lit("GetAccessionRightsTransferred(id)"), :rightsTransferred), + Sequel.as(Sequel.lit("GetAccessionRightsTransferredNote(id)"), :rightsTransferredNote), + Sequel.as(Sequel.lit("GetAccessionAcknowledgementSent(id)"), :acknowledgementSent)) + end + +end diff --git a/reports/accessions/accession_report/en.yml b/reports/accessions/accession_report/en.yml new file mode 100644 index 0000000000..7f1770aff5 --- /dev/null +++ b/reports/accessions/accession_report/en.yml @@ -0,0 +1,31 @@ +en: + reports: + accession_report: + title: Accession Report + description: Report on Accession records + number_of_accessions: Number of accessions + identifier_prefix: Accession + accession_date: Accession Date + extent: Extent + container: Container + deaccessions: Deaccessions + locations: Locations + general_note: General Note + begin_date: Begin Date + end_date: End Date + bulk_begin_date: Bulk Begin Date + bulk_end_date: Bulk End Date + date_expression: Date Expression + names: Names + subjects: Subjects + acquisitions: Acquisitions + retention: Retention + description: Description + condition: Condition + inventory: Inventory + disposition: Disposition + restrictions_apply: Retrictions Apply? + access_restrictions_note: Access Restrictions Note + user_restrictions_note: Use Restrictions Note + rights_transferred: Rights Transferred? + rights_transferred_note: Rights Transferred Note \ No newline at end of file diff --git a/reports/accessions/accession_resources_subreport/accession_resources_subreport.erb b/reports/accessions/accession_resources_subreport/accession_resources_subreport.erb new file mode 100644 index 0000000000..d7fa95fc34 --- /dev/null +++ b/reports/accessions/accession_resources_subreport/accession_resources_subreport.erb @@ -0,0 +1,11 @@ + + + + + <% @report.each do |record| %> + + + + + <% end %> +
      <%= t('identifier') %><%= t('title') %>
      <%= format_4part(record.fetch('identifier')) %><%= transform_text(record.fetch('title')) %>
      diff --git a/reports/accessions/accession_resources_subreport/accession_resources_subreport.rb b/reports/accessions/accession_resources_subreport/accession_resources_subreport.rb new file mode 100644 index 0000000000..24c4367037 --- /dev/null +++ b/reports/accessions/accession_resources_subreport/accession_resources_subreport.rb @@ -0,0 +1,17 @@ +class AccessionResourcesSubreport < AbstractReport + + def template + "accession_resources_subreport.erb" + end + + def query + relationships = db[:spawned_rlshp]. + filter(:spawned_rlshp__accession_id => @params.fetch(:accessionId)) + + db[:resource] + .filter(:id => relationships.select(:resource_id)) + .select(Sequel.as(:identifier, :identifier), + Sequel.as(:title, :title)) + end + +end diff --git a/reports/accessions/accession_resources_subreport/en.yml b/reports/accessions/accession_resources_subreport/en.yml new file mode 100644 index 0000000000..d3c1b69c01 --- /dev/null +++ b/reports/accessions/accession_resources_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + accession_resources_subreport: + identifier: Identifier + title: Title diff --git a/reports/accessions/accession_rights_transferred_report/accession_rights_transferred_report.erb b/reports/accessions/accession_rights_transferred_report/accession_rights_transferred_report.erb new file mode 100644 index 0000000000..2a717d5082 --- /dev/null +++ b/reports/accessions/accession_rights_transferred_report/accession_rights_transferred_report.erb @@ -0,0 +1,56 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('number_of_records') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('total_transferred') %>
      +
      <%= h @report.total_transferred %>
      +
      +
      + +<% @report.each do |accession| %> + <% next if accession.fetch('rightsTransferred') == 0 %> + +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      +
      <%= t('accession_date') %>
      +
      <%= format_date(accession.fetch('accessionDate')) %>
      +
      <%= t('processed_date') %>
      +
      <%= format_date(accession.fetch('accessionProcessedDate')) %>
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      <%= t('cataloged') %>
      +
      <%= format_boolean(accession.fetch('cataloged') == 1) %>
      +
      <%= t('rights_and_restrictions') %>
      +
      + <% if accession.fetch('restrictionsApply') == 1 %> + <%= t('retrictions_apply') %> + <% else %> + <%= t('no_restrictions') %> + <% end %> +
      +
      <%= t('rights_transferred') %>
      +
      + <% if accession.fetch('rightsTransferred') == 1 %> + <%= t('rights_transferred_true') %> + <% else %> + <%= t('rights_transferred_false') %> + <% end %> +
      +
      + + <%= text_section(t('rights_transferred_note'), accession.fetch('rightsTransferredNote')) %> + + <%= text_section(t('use_restrictions_note'), accession.fetch('useRestrictionsNote')) %> + + <%= text_section(t('access_restrictions_note'), accession.fetch('accessRestrictionsNote')) %> + + <%= subreport_section(t('linked_resources'), AccessionResourcesSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_rights_transferred_report/accession_rights_transferred_report.rb b/reports/accessions/accession_rights_transferred_report/accession_rights_transferred_report.rb new file mode 100644 index 0000000000..85810dd065 --- /dev/null +++ b/reports/accessions/accession_rights_transferred_report/accession_rights_transferred_report.rb @@ -0,0 +1,41 @@ +class AccessionRightsTransferredReport < AbstractReport + + register_report + + def template + 'accession_rights_transferred_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(:restrictions_apply, :restrictionsApply), + Sequel.as(:access_restrictions, :accessRestrictions), + Sequel.as(:access_restrictions_note, :accessRestrictionsNote), + Sequel.as(:use_restrictions, :useRestrictions), + Sequel.as(:use_restrictions_note, :useRestrictionsNote), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType), + Sequel.as(Sequel.lit('GetAccessionRightsTransferred(id)'), :rightsTransferred), + Sequel.as(Sequel.lit('GetAccessionRightsTransferredNote(id)'), :rightsTransferredNote)) + end + + # Number of Records Reviewed + def total_count + @total_count ||= self.query.count + end + + # Accessions with Rights Transferred + def total_transferred + @total_transferred ||= db.from(self.query).where(:rightsTransferred => 1).count + end + +end diff --git a/reports/accessions/accession_rights_transferred_report/en.yml b/reports/accessions/accession_rights_transferred_report/en.yml new file mode 100644 index 0000000000..219d966454 --- /dev/null +++ b/reports/accessions/accession_rights_transferred_report/en.yml @@ -0,0 +1,22 @@ +en: + reports: + accession_rights_transferred_report: + title: Accessions with rights transferred + description: Displays only those accession(s) for which rights have been transferred. Report contains accession number, linked resources, title, extent, cataloged, date processed, access restrictions, use restrictions, rights transferred and a count of the number of records selected with rights transferred. + number_of_records: Number of Records + total_transferred: Accessions with Rights Transferred + identifier_prefix: Accession + accession_date: Accession Date + processed_date: Date Processed + extent: Extent + cataloged: Cataloged + rights_and_restrictions: Rights and Restrictions + retrictions_apply: Restrictions apply. + no_restrictions: No restrictions. + rights_transferred: Rights Transferred + rights_transferred_true: Rights Transferred + rights_transferred_false: Not transferred + rights_transferred_note: Rights Transferred Note + use_restrictions_note: Use Restrictions Note + access_restrictions_note: Access Restrictions Note + linked_resources: Linked Resource(s) \ No newline at end of file diff --git a/reports/accessions/accession_subjects_names_classifications_list_report/accession_subjects_names_classifications_list_report.erb b/reports/accessions/accession_subjects_names_classifications_list_report/accession_subjects_names_classifications_list_report.erb new file mode 100644 index 0000000000..a91c33b0c5 --- /dev/null +++ b/reports/accessions/accession_subjects_names_classifications_list_report/accession_subjects_names_classifications_list_report.erb @@ -0,0 +1,21 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('number_of_records') %>
      +
      <%= h @report.total_count %>
      +
      +
      + +<% @report.each do |accession| %> +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + + <%= subreport_section(t('names'), AccessionNamesSubreport, accession) %> + + <%= subreport_section(t('subjects'), AccessionSubjectsSubreport, accession) %> + + <%= subreport_section(t('classifications'), AccessionClassificationsSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_subjects_names_classifications_list_report/accession_subjects_names_classifications_list_report.rb b/reports/accessions/accession_subjects_names_classifications_list_report/accession_subjects_names_classifications_list_report.rb new file mode 100644 index 0000000000..c4d192a1f2 --- /dev/null +++ b/reports/accessions/accession_subjects_names_classifications_list_report/accession_subjects_names_classifications_list_report.rb @@ -0,0 +1,36 @@ +class AccessionSubjectsNamesClassificationsListReport < AbstractReport + + register_report + + def template + 'accession_subjects_names_classifications_list_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(:restrictions_apply, :restrictionsApply), + Sequel.as(:access_restrictions, :accessRestrictions), + Sequel.as(:access_restrictions_note, :accessRestrictionsNote), + Sequel.as(:use_restrictions, :useRestrictions), + Sequel.as(:use_restrictions_note, :useRestrictionsNote), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType), + Sequel.as(Sequel.lit('GetAccessionRightsTransferred(id)'), :rightsTransferred), + Sequel.as(Sequel.lit('GetAccessionRightsTransferredNote(id)'), :rightsTransferredNote)) + end + + # Number of Records Reviewed + def total_count + @total_count ||= self.query.count + end + +end diff --git a/reports/accessions/accession_subjects_names_classifications_list_report/en.yml b/reports/accessions/accession_subjects_names_classifications_list_report/en.yml new file mode 100644 index 0000000000..dbf651c65e --- /dev/null +++ b/reports/accessions/accession_subjects_names_classifications_list_report/en.yml @@ -0,0 +1,10 @@ +en: + reports: + accession_subjects_names_classifications_list_report: + title: Accessions and Linked Subjects, Names and Classifications + description: Displays accessions and their linked names, subjects, and classifications. Report contains accession number, linked resources, accession date, title, extent, linked names, and linked subjects. + number_of_records: Number of Records + identifier_prefix: Accession + names: Names + subjects: Subjects + classifications: Classifications \ No newline at end of file diff --git a/reports/accessions/accession_subjects_subreport/accession_subjects_subreport.erb b/reports/accessions/accession_subjects_subreport/accession_subjects_subreport.erb new file mode 100644 index 0000000000..0a4fdeefcf --- /dev/null +++ b/reports/accessions/accession_subjects_subreport/accession_subjects_subreport.erb @@ -0,0 +1,13 @@ + + + + + + <% @report.each do |subject| %> + + + + + + <% end %> +
      <%= t('term') %><%= t('type') %><%= t('source') %>
      <%= h subject.fetch('subjectTerm') %><%= h subject.fetch('subjectTermType') %><%= h subject.fetch('subjectSource') %>
      diff --git a/reports/accessions/accession_subjects_subreport/accession_subjects_subreport.rb b/reports/accessions/accession_subjects_subreport/accession_subjects_subreport.rb new file mode 100644 index 0000000000..d684666225 --- /dev/null +++ b/reports/accessions/accession_subjects_subreport/accession_subjects_subreport.rb @@ -0,0 +1,17 @@ +class AccessionSubjectsSubreport < AbstractReport + + def template + "accession_subjects_subreport.erb" + end + + def query + db[:subject_rlshp] + .join(:subject, :id => :subject_id) + .filter(:accession_id => @params.fetch(:accessionId)) + .select(Sequel.as(:subject__id, :subject_id), + Sequel.as(:subject__title, :subjectTerm), + Sequel.as(Sequel.lit("GetTermType(subject.id)"), :subjectTermType), + Sequel.as(Sequel.lit("GetEnumValue(subject.source_id)"), :subjectSource)) + end + +end diff --git a/reports/accessions/accession_subjects_subreport/en.yml b/reports/accessions/accession_subjects_subreport/en.yml new file mode 100644 index 0000000000..57f230f1e0 --- /dev/null +++ b/reports/accessions/accession_subjects_subreport/en.yml @@ -0,0 +1,6 @@ +en: + reports: + accession_subjects_subreport: + term: Term + type: Type + source: Source \ No newline at end of file diff --git a/reports/accessions/accession_uncataloged_report/accession_uncataloged_report.erb b/reports/accessions/accession_uncataloged_report/accession_uncataloged_report.erb new file mode 100644 index 0000000000..29c05d060f --- /dev/null +++ b/reports/accessions/accession_uncataloged_report/accession_uncataloged_report.erb @@ -0,0 +1,33 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('uncataloged_count') %>
      +
      <%= h @report.uncataloged_count %>
      +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      +
      + +
      + +<% @report.each do |accession| %> + <% next if accession.fetch('cataloged') == 1 %> + +
      +
      <%= t('identifier_prefix') %> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent') %>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      <%= t('date_processed') %>
      +
      <%= format_date(accession.fetch('accessionProcessedDate')) %>
      +
      + + <%= subreport_section(t('linked_resources'), AccessionResourcesSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_uncataloged_report/accession_uncataloged_report.rb b/reports/accessions/accession_uncataloged_report/accession_uncataloged_report.rb new file mode 100644 index 0000000000..795d4191e2 --- /dev/null +++ b/reports/accessions/accession_uncataloged_report/accession_uncataloged_report.rb @@ -0,0 +1,39 @@ +class AccessionUncatalogedReport < AbstractReport + + register_report + + def template + 'accession_uncataloged_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionCatalogedDate(id)'), :catalogedDate), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + + # Number of Records Reviewed + def total_count + @totalCount ||= self.query.count + end + + # Uncataloged Accessions + def uncataloged_count + @uncatalogedCount ||= db.from(self.query).where(Sequel.~(:cataloged => 1)).count + end + + # Total Extent of Uncataloged Accessions + def total_extent + @totalExtent ||= db.from(self.query).where(Sequel.~(:cataloged => 1)).sum(:extentNumber) + end +end diff --git a/reports/accessions/accession_uncataloged_report/en.yml b/reports/accessions/accession_uncataloged_report/en.yml new file mode 100644 index 0000000000..5ebdf430fe --- /dev/null +++ b/reports/accessions/accession_uncataloged_report/en.yml @@ -0,0 +1,12 @@ +en: + reports: + accession_uncataloged_report: + title: Uncataloged Accessions + description: Displays only those accession(s) that have not been checked as cataloged. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected that are not checked as cataloged, and the total extent number for those records not cataloged. + total_count: Number of Records Reviewed + uncataloged_count: Uncataloged Accessions + total_extent: Total Extent of Unataloged Accessions + identifier_prefix: Accession + extent: Extent + date_processed: Date Processed + linked_resources: Linked Resource(s) \ No newline at end of file diff --git a/reports/accessions/accession_unprocessed_report/accession_unprocessed_report.erb b/reports/accessions/accession_unprocessed_report/accession_unprocessed_report.erb new file mode 100644 index 0000000000..d92b62d645 --- /dev/null +++ b/reports/accessions/accession_unprocessed_report/accession_unprocessed_report.erb @@ -0,0 +1,34 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count')%>
      +
      <%= h @report.total_count %>
      +
      <%= t('total_unprocessed')%>
      +
      <%= h @report.total_unprocessed %>
      +
      <%= t('between')%>
      +
      <%= format_date(@report.from_date) %> &amp; <%= format_date(@report.to_date) %>
      +
      <%= t('total_extent_of_unprocessed')%>
      +
      <%= format_number(@report.total_extent_of_unprocessed) %>
      +
      +
      + +<% @report.each do |accession| %> + <% next unless accession.fetch('accessionProcessed') != '1' %> + +
      +
      <%= t('identifier_prefix')%> <%= format_4part(accession.fetch('accessionNumber')) %>
      +
      <%= h accession.fetch('title') %>
      + +
      + <% if accession.fetch('extentNumber') %> +
      <%= t('extent')%>
      +
      <%= format_number(accession.fetch('extentNumber')) %> <%= accession.fetch('extentType') %>
      + <% end %> +
      <%= t('cataloged')%>
      +
      <%= format_boolean(accession.fetch('cataloged') == 1) %>
      +
      + + <%= subreport_section(t('linked_resource'), AccessionResourcesSubreport, accession) %> +
      +<% end %> diff --git a/reports/accessions/accession_unprocessed_report/accession_unprocessed_report.rb b/reports/accessions/accession_unprocessed_report/accession_unprocessed_report.rb new file mode 100644 index 0000000000..783f1418b3 --- /dev/null +++ b/reports/accessions/accession_unprocessed_report/accession_unprocessed_report.rb @@ -0,0 +1,48 @@ +class AccessionUnprocessedReport < AbstractReport + + register_report + + def template + 'accession_unprocessed_report.erb' + end + + def query + db[:accession]. + select(Sequel.as(:id, :accessionId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:identifier, :accessionNumber), + Sequel.as(:title, :title), + Sequel.as(:accession_date, :accessionDate), + Sequel.as(Sequel.lit('GetAccessionContainerSummary(id)'), :containerSummary), + Sequel.as(Sequel.lit('GetAccessionProcessed(id)'), :accessionProcessed), + Sequel.as(Sequel.lit('GetAccessionProcessedDate(id)'), :accessionProcessedDate), + Sequel.as(Sequel.lit('GetAccessionCataloged(id)'), :cataloged), + Sequel.as(Sequel.lit('GetAccessionExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetAccessionExtentType(id)'), :extentType)) + end + + # Number of Records Reviewed + def total_count + @total_count ||= self.query.count + end + + # Unprocessed Accessions + def total_unprocessed + @total_processed ||= db.from(self.query).where(Sequel.~(:accessionProcessed => 1)).count + end + + # Accessioned Between - From Date + def from_date + @from_date ||= self.query.min(:accession_date) + end + + # Accessioned Between - To Date + def to_date + @to_date ||= self.query.max(:accession_date) + end + + #Total Extent of Unprocessed Accessions + def total_extent_of_unprocessed + @total_extent_of_processed ||= db.from(self.query).where(Sequel.~(:accessionProcessed => 1)).sum(:extentNumber) + end +end diff --git a/reports/accessions/accession_unprocessed_report/en.yml b/reports/accessions/accession_unprocessed_report/en.yml new file mode 100644 index 0000000000..4b994dc42c --- /dev/null +++ b/reports/accessions/accession_unprocessed_report/en.yml @@ -0,0 +1,13 @@ +en: + reports: + accession_unprocessed_report: + title: Unprocessed Accessions + description: Displays only those accession(s) that have not been processed. Report contains accession number, linked resources, title, extent, cataloged, date processed, a count of the number of records selected with date processed, and the total extent number for those records without a completed date processed field. + total_count: Number of Accessions + total_unprocessed: Unprocessed Accessions + between: Accessioned Between + total_extent_of_unprocessed: Total Extent of Unprocessed Accessions + identifier_prefix: Accession + extent: Extent + cataloged: Cataloged + linked_resources: Linked Resource(s) diff --git a/backend/app/model/reports/created_accessions_report.rb b/reports/accessions/created_accessions_report/created_accessions_report.rb similarity index 78% rename from backend/app/model/reports/created_accessions_report.rb rename to reports/accessions/created_accessions_report/created_accessions_report.rb index 69c3c7cf7f..50d7d0be98 100644 --- a/backend/app/model/reports/created_accessions_report.rb +++ b/reports/accessions/created_accessions_report/created_accessions_report.rb @@ -1,13 +1,11 @@ class CreatedAccessionsReport < AbstractReport register_report({ - :uri_suffix => "created_accessions", - :description => "Report on accessions created within a date range", :params => [["from", Date, "The start of report range"], ["to", Date, "The start of report range"]] }) - def initialize(params, job) + def initialize(params, job, db) super from = params["from"] || Time.now.to_s to = params["to"] || Time.now.to_s @@ -17,10 +15,6 @@ def initialize(params, job) end - def title - "Accessions created between #{@from} and #{@to}" - end - def headers ['id', 'identifier', 'title', 'create_date', 'create_time'] end @@ -33,7 +27,7 @@ def processor } end - def query(db) + def query db[:accession].where(:create_time => (@from..@to)).order(Sequel.asc(:create_time)) end diff --git a/reports/accessions/created_accessions_report/en.yml b/reports/accessions/created_accessions_report/en.yml new file mode 100644 index 0000000000..596779800e --- /dev/null +++ b/reports/accessions/created_accessions_report/en.yml @@ -0,0 +1,6 @@ +en: + reports: + created_accessions_report: + title: Created Accessions + description: Report on accessions created within a date range + full_title: Accessions created between %{from} and %{to} diff --git a/reports/accessions/unprocessed_accessions_report/en.yml b/reports/accessions/unprocessed_accessions_report/en.yml new file mode 100644 index 0000000000..e3bb372d0a --- /dev/null +++ b/reports/accessions/unprocessed_accessions_report/en.yml @@ -0,0 +1,5 @@ +en: + reports: + unprocessed_accessions_report: + title: Unprocessed Accessions + description: Report on all unprocessed accessions \ No newline at end of file diff --git a/backend/app/model/reports/unprocessed_accessions_report.rb b/reports/accessions/unprocessed_accessions_report/unprocessed_accessions_report.rb similarity index 87% rename from backend/app/model/reports/unprocessed_accessions_report.rb rename to reports/accessions/unprocessed_accessions_report/unprocessed_accessions_report.rb index 530b208f88..0e1de5991e 100644 --- a/backend/app/model/reports/unprocessed_accessions_report.rb +++ b/reports/accessions/unprocessed_accessions_report/unprocessed_accessions_report.rb @@ -1,13 +1,6 @@ -#noinspection ALL class UnprocessedAccessionsReport < AbstractReport - register_report({ - :uri_suffix => "unprocessed_accessions", - :description => "Report on all unprocessed accessions", - }) - def initialize(params, job) - super - end + register_report def headers ['id', 'identifier', 'title', "processing_priority", "processing_status", "processors"] @@ -19,12 +12,7 @@ def processor } end - def scope_by_repo_id(dataset) - # repo scope is applied in the query below - dataset - end - - def query(db) + def query dataset = db[:accession]. left_outer_join(:collection_management, :accession_id => :id). join(:enumeration, diff --git a/reports/agents/agent_list_report/agent_list_report.rb b/reports/agents/agent_list_report/agent_list_report.rb new file mode 100644 index 0000000000..4f2e0c593b --- /dev/null +++ b/reports/agents/agent_list_report/agent_list_report.rb @@ -0,0 +1,45 @@ +class AgentListReport < AbstractReport + + register_report + + def template + 'generic_listing.erb' + end + + def headers + ['sortName', 'nameType', 'nameSource'] + end + + def query + people = db[:name_person] + .filter(:name_person__is_display_name => 1) + .filter(Sequel.~(:name_person__source_id => nil)) + .filter(db[:user] + .filter(:user__agent_record_id => :name_person__agent_person_id) + .select(:agent_record_id) => nil) + .select(Sequel.as(:agent_person_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Person', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_person.source_id)'), :nameSource)) + + families = db[:name_family] + .filter(:name_family__is_display_name => 1) + .filter(Sequel.~(:name_family__source_id => nil)) + .select(Sequel.as(:agent_family_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Family', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_family.source_id)'), :nameSource)) + + corporate = db[:name_corporate_entity] + .filter(:name_corporate_entity__is_display_name => 1) + .filter(Sequel.~(:name_corporate_entity__source_id => nil)) + .select(Sequel.as(:agent_corporate_entity_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Corporate', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_corporate_entity.source_id)'), :nameSource)) + + people + .union(families) + .union(corporate) + end +end diff --git a/reports/agents/agent_list_report/en.yml b/reports/agents/agent_list_report/en.yml new file mode 100644 index 0000000000..c2745e2d98 --- /dev/null +++ b/reports/agents/agent_list_report/en.yml @@ -0,0 +1,5 @@ +en: + reports: + agent_list_report: + title: Name Records List + description: Displays selected name record(s). Report lists name, name type, and name source. \ No newline at end of file diff --git a/reports/agents/agent_name_non_preferred_subreport/agent_name_non_preferred_subreport.erb b/reports/agents/agent_name_non_preferred_subreport/agent_name_non_preferred_subreport.erb new file mode 100644 index 0000000000..9c2fc04c5a --- /dev/null +++ b/reports/agents/agent_name_non_preferred_subreport/agent_name_non_preferred_subreport.erb @@ -0,0 +1,11 @@ + + + + + <% @report.each do |name| %> + + + + + <% end %> +
      <%= t('name') %><%= t('source') %>
      <%= h name.fetch('sortName') %><%= h name.fetch('nameSource') %>
      diff --git a/reports/agents/agent_name_non_preferred_subreport/agent_name_non_preferred_subreport.rb b/reports/agents/agent_name_non_preferred_subreport/agent_name_non_preferred_subreport.rb new file mode 100644 index 0000000000..696fb9069c --- /dev/null +++ b/reports/agents/agent_name_non_preferred_subreport/agent_name_non_preferred_subreport.rb @@ -0,0 +1,41 @@ +class AgentNameNonPreferredSubreport < AbstractReport + + def template + 'agent_name_non_preferred_subreport.erb' + end + + def query + name_type = @params.fetch(:nameType) + + if name_type == 'Person' + db[:name_person] + .filter(:name_person__agent_person_id => @params.fetch(:agentId)) + .filter(Sequel.~(:name_person__source_id => nil)) + .filter(:name_person__is_display_name => nil) + .select(Sequel.as(:agent_person_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Person', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_person.source_id)'), :nameSource)) + elsif name_type == 'Family' + db[:name_family] + .filter(:name_family__is_display_name => nil) + .filter(Sequel.~(:name_family__source_id => nil)) + .filter(:name_family__agent_family_id => @params.fetch(:agentId)) + .select(Sequel.as(:agent_family_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Family', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_family.source_id)'), :nameSource)) + elsif name_type == 'Corporate' + db[:name_corporate_entity] + .filter(:name_corporate_entity__is_display_name => nil) + .filter(Sequel.~(:name_corporate_entity__source_id => nil)) + .filter(:name_corporate_entity__agent_corporate_entity_id => @params.fetch(:agentId)) + .select(Sequel.as(:agent_corporate_entity_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Corporate', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_corporate_entity.source_id)'), :nameSource)) + else + raise "nameType not recognised: #{name_type}" + end + end +end diff --git a/reports/agents/agent_name_non_preferred_subreport/en.yml b/reports/agents/agent_name_non_preferred_subreport/en.yml new file mode 100644 index 0000000000..3041d2f7d7 --- /dev/null +++ b/reports/agents/agent_name_non_preferred_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + agent_name_non_preferred_subreport: + name: Name + source: Source diff --git a/reports/agents/agent_name_to_non_preferred_report/agent_name_to_non_preferred_report.erb b/reports/agents/agent_name_to_non_preferred_report/agent_name_to_non_preferred_report.erb new file mode 100644 index 0000000000..42bc0aa699 --- /dev/null +++ b/reports/agents/agent_name_to_non_preferred_report/agent_name_to_non_preferred_report.erb @@ -0,0 +1,23 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count') %>
      +
      <%= h @report.total_count %>
      +
      +
      + +<% @report.each do |name| %> +
      +
      <%= h name.fetch('sortName') %>
      + +
      +
      <%= t('name_type') %>
      +
      <%= h name.fetch('nameType') %>
      +
      <%= t('source') %>
      +
      <%= h name.fetch('nameSource') %>
      +
      + + <%= subreport_section(t('non_preferred_names'), AgentNameNonPreferredSubreport, name) %> +
      +<% end %> diff --git a/reports/agents/agent_name_to_non_preferred_report/agent_name_to_non_preferred_report.rb b/reports/agents/agent_name_to_non_preferred_report/agent_name_to_non_preferred_report.rb new file mode 100644 index 0000000000..56ab8065bb --- /dev/null +++ b/reports/agents/agent_name_to_non_preferred_report/agent_name_to_non_preferred_report.rb @@ -0,0 +1,45 @@ +class AgentNameToNonPreferredReport < AbstractReport + + register_report + + def template + 'agent_name_to_non_preferred_report.erb' + end + + def query + people = db[:name_person] + .filter(:name_person__is_display_name => 1) + .filter(Sequel.~(:name_person__source_id => nil)) + .filter(db[:user] + .filter(:user__agent_record_id => :name_person__agent_person_id) + .select(:agent_record_id) => nil) + .select(Sequel.as(:agent_person_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Person', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_person.source_id)'), :nameSource)) + + families = db[:name_family] + .filter(:name_family__is_display_name => 1) + .filter(Sequel.~(:name_family__source_id => nil)) + .select(Sequel.as(:agent_family_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Family', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_family.source_id)'), :nameSource)) + + corporate = db[:name_corporate_entity] + .filter(:name_corporate_entity__is_display_name => 1) + .filter(Sequel.~(:name_corporate_entity__source_id => nil)) + .select(Sequel.as(:agent_corporate_entity_id, :agentId), + Sequel.as(:sort_name, :sortName), + Sequel.as('Corporate', :nameType), + Sequel.as(Sequel.lit('GetEnumValueUF(name_corporate_entity.source_id)'), :nameSource)) + + people + .union(families) + .union(corporate) + end + + def total_count + @count ||= query.count + end +end diff --git a/reports/agents/agent_name_to_non_preferred_report/en.yml b/reports/agents/agent_name_to_non_preferred_report/en.yml new file mode 100644 index 0000000000..dd2278c4cb --- /dev/null +++ b/reports/agents/agent_name_to_non_preferred_report/en.yml @@ -0,0 +1,9 @@ +en: + reports: + agent_name_to_non_preferred_report: + title: Name Records and Non-preferred Forms + description: Displays name(s) and any associated non-preferred name(s). Report contains sort name, name type, name source and any other non-preferred forms of that name. + total_count: Number of Records + name_type: Name Type + source: Source + non_preferred_names: Non-preferred form(s) \ No newline at end of file diff --git a/reports/digital_objects/digital_object_component_file_versions_list_subreport/digital_object_component_file_versions_list_subreport.erb b/reports/digital_objects/digital_object_component_file_versions_list_subreport/digital_object_component_file_versions_list_subreport.erb new file mode 100644 index 0000000000..3ef2fb59c4 --- /dev/null +++ b/reports/digital_objects/digital_object_component_file_versions_list_subreport/digital_object_component_file_versions_list_subreport.erb @@ -0,0 +1,19 @@ +<% @report.each do |digital_object_component| %> + <% file_versions = @report.file_versions_for(digital_object_component.fetch('digitalObjectComponentId')) %> + <% if file_versions.length > 0 %> + + <%= h digital_object_component.fetch('digitalObjectComponentIdentifier') %> + <%= transform_text(digital_object_component.fetch('digitalObjectComponentTitle')) %> + <%= h file_versions.first.fetch(:uri) %> + <%= h file_versions.first.fetch(:useStatement) %> + + <% file_versions[1..file_versions.length].each do |file_version| %> + + + <%= h file_version.fetch(:uri) %> + <%= h file_version.fetch(:useStatement) %> + + <% end %> + <% end %> + <%= insert_subreport(DigitalObjectComponentFileVersionsListSubreport, digital_object_component) %> +<% end %> diff --git a/reports/digital_objects/digital_object_component_file_versions_list_subreport/digital_object_component_file_versions_list_subreport.rb b/reports/digital_objects/digital_object_component_file_versions_list_subreport/digital_object_component_file_versions_list_subreport.rb new file mode 100644 index 0000000000..fc20133093 --- /dev/null +++ b/reports/digital_objects/digital_object_component_file_versions_list_subreport/digital_object_component_file_versions_list_subreport.rb @@ -0,0 +1,41 @@ +class DigitalObjectComponentFileVersionsListSubreport < AbstractReport + + def template + "digital_object_component_file_versions_list_subreport.erb" + end + + def query + components = db[:digital_object_component] + .select(Sequel.as(:digital_object_component__id, :digitalObjectComponentId), + Sequel.as(:digital_object_component__component_id, :digitalObjectComponentIdentifier), + Sequel.as(:digital_object_component__title, :digitalObjectComponentTitle)) + .order(:digital_object_component__position) + + if component_level? + components. + filter(:parent_id => @params.fetch(:digitalObjectComponentId)) + else + components. + filter(:parent_id => nil). + and(:root_record_id => @params.fetch(:digitalObjectId)) + end + end + + def component_level? + @params.has_key?(:digitalObjectComponentId) + end + + def show_wrapper_html?(index) + !component_level? && index == 0 + end + + def file_versions_for(digital_object_component_id) + db[:file_version] + .join(:digital_object_component, :id => :digital_object_component_id) + .filter(:digital_object_component_id => digital_object_component_id) + .select(Sequel.as(:file_version__id, :file_version_id), + Sequel.as(:file_version__file_uri, :uri), + Sequel.as(Sequel.lit("GetEnumValue(file_version.use_statement_id)"), :useStatement)) + .all + end +end diff --git a/reports/digital_objects/digital_object_component_file_versions_list_subreport/en.yml b/reports/digital_objects/digital_object_component_file_versions_list_subreport/en.yml new file mode 100644 index 0000000000..ce557beea3 --- /dev/null +++ b/reports/digital_objects/digital_object_component_file_versions_list_subreport/en.yml @@ -0,0 +1,3 @@ +en: + reports: + digital_object_component_file_versions_list_subreport: diff --git a/reports/digital_objects/digital_object_file_versions_list_subreport/digital_object_file_versions_list_subreport.erb b/reports/digital_objects/digital_object_file_versions_list_subreport/digital_object_file_versions_list_subreport.erb new file mode 100644 index 0000000000..5d52108cbb --- /dev/null +++ b/reports/digital_objects/digital_object_file_versions_list_subreport/digital_object_file_versions_list_subreport.erb @@ -0,0 +1,11 @@ + + + + + <% @report.each do |file_version| %> + + + + + <% end %> +
      <%= t('uri') %><%= t('use_statement') %>
      <%= h file_version.fetch('uri') %><%= h file_version.fetch('useStatement') %>
      diff --git a/reports/digital_objects/digital_object_file_versions_list_subreport/digital_object_file_versions_list_subreport.rb b/reports/digital_objects/digital_object_file_versions_list_subreport/digital_object_file_versions_list_subreport.rb new file mode 100644 index 0000000000..1066e327f4 --- /dev/null +++ b/reports/digital_objects/digital_object_file_versions_list_subreport/digital_object_file_versions_list_subreport.rb @@ -0,0 +1,16 @@ +class DigitalObjectFileVersionsListSubreport < AbstractReport + + def template + "digital_object_file_versions_list_subreport.erb" + end + + def query + db[:file_version] + .join(:digital_object, :id => :digital_object_id) + .filter(:digital_object__id => @params.fetch(:digitalObjectId)) + .select(Sequel.as(:file_version__id, :file_version_id), + Sequel.as(:file_version__file_uri, :uri), + Sequel.as(Sequel.lit("GetEnumValue(file_version.use_statement_id)"), :useStatement)) + end + +end diff --git a/reports/digital_objects/digital_object_file_versions_list_subreport/en.yml b/reports/digital_objects/digital_object_file_versions_list_subreport/en.yml new file mode 100644 index 0000000000..b73ebc7c31 --- /dev/null +++ b/reports/digital_objects/digital_object_file_versions_list_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + digital_object_file_versions_list_subreport: + uri: URI + use_statement: Use Statement diff --git a/reports/digital_objects/digital_object_file_versions_report/digital_object_file_versions_report.erb b/reports/digital_objects/digital_object_file_versions_report/digital_object_file_versions_report.erb new file mode 100644 index 0000000000..f3d6915afb --- /dev/null +++ b/reports/digital_objects/digital_object_file_versions_report/digital_object_file_versions_report.erb @@ -0,0 +1,34 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count') %>
      +
      <%= h @report.total_count %>
      +
      +
      + +<% @report.each do |digital_object| %> +
      +
      <%= t('identifier_prefix') %> <%= h digital_object.fetch('identifier') %>
      +
      <%= h digital_object.fetch('title') %>
      + + <%= subreport_section(t('file_versions'), DigitalObjectFileVersionsListSubreport, digital_object) %> + +
      +

      <%= t('components') %>

      + + + + + + + + + + + <%= insert_subreport(DigitalObjectComponentFileVersionsListSubreport, digital_object) %> + +
      <%= t('component_identifier') %><%= t('component_title') %><%= t('uri') %><%= t('use_statement') %>
      +
      +
      +<% end %> diff --git a/reports/digital_objects/digital_object_file_versions_report/digital_object_file_versions_report.rb b/reports/digital_objects/digital_object_file_versions_report/digital_object_file_versions_report.rb new file mode 100644 index 0000000000..6533fd97c1 --- /dev/null +++ b/reports/digital_objects/digital_object_file_versions_report/digital_object_file_versions_report.rb @@ -0,0 +1,22 @@ +class DigitalObjectFileVersionsReport < AbstractReport + + register_report + + def template + 'digital_object_file_versions_report.erb' + end + + + def query + db[:digital_object]. + select(Sequel.as(:id, :digitalObjectId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:digital_object_id, :identifier), + Sequel.as(:title, :title)) + end + + # Number of Records + def total_count + @total_count ||= self.query.count + end +end diff --git a/reports/digital_objects/digital_object_file_versions_report/en.yml b/reports/digital_objects/digital_object_file_versions_report/en.yml new file mode 100644 index 0000000000..dcba7a9527 --- /dev/null +++ b/reports/digital_objects/digital_object_file_versions_report/en.yml @@ -0,0 +1,13 @@ +en: + reports: + digital_object_file_versions_report: + title: File Version List + description: Displays any file versions associated with the selected digital objects. + total_count: Number of Records + identifier_prefix: Digital Object + file_versions: File Version(s) + components: Component(s) + component_identifier: Component Identifier + component_title: Component Title + uri: URI + use_statement: Use Statement \ No newline at end of file diff --git a/reports/digital_objects/digital_object_list_table_report/digital_object_list_table_report.rb b/reports/digital_objects/digital_object_list_table_report/digital_object_list_table_report.rb new file mode 100644 index 0000000000..c664c0eea9 --- /dev/null +++ b/reports/digital_objects/digital_object_list_table_report/digital_object_list_table_report.rb @@ -0,0 +1,31 @@ +class DigitalObjectListTableReport < AbstractReport + + register_report + + def template + 'generic_listing.erb' + end + + def headers + ['title', 'identifier', 'objectType', 'dateExpression', 'resourceIdentifier'] + end + + def processor + { + 'resourceIdentifier' => proc{|record| ASUtils.json_parse(record[:resourceIdentifier] || '[]').compact.join('.')} + } + end + + def query + db[:digital_object]. + left_outer_join(:instance_do_link_rlshp, + :instance_do_link_rlshp__digital_object_id => :digital_object__id). + select(Sequel.as(:digital_object__id, :id), + Sequel.as(:digital_object__repo_id, :repoId), + Sequel.as(:digital_object__digital_object_id, :identifier), + Sequel.as(:digital_object__title, :title), + Sequel.as(Sequel.lit('GetEnumValueUF(digital_object.digital_object_type_id)'), :objectType), + Sequel.as(Sequel.lit('GetDigitalObjectDateExpression(digital_object.id)'), :dateExpression), + Sequel.as(Sequel.lit('GetResourceIdentiferForInstance(instance_do_link_rlshp.instance_id)'), :resourceIdentifier)) + end +end diff --git a/reports/digital_objects/digital_object_list_table_report/en.yml b/reports/digital_objects/digital_object_list_table_report/en.yml new file mode 100644 index 0000000000..c078928197 --- /dev/null +++ b/reports/digital_objects/digital_object_list_table_report/en.yml @@ -0,0 +1,5 @@ +en: + reports: + digital_object_list_table_report: + title: Digital Object table + description: Displays digital object(s) in table format. Report contains title, digital object identifier, object type, dates and the title and identifier of linked resources. \ No newline at end of file diff --git a/reports/locations/location_accessions_subreport/en.yml b/reports/locations/location_accessions_subreport/en.yml new file mode 100644 index 0000000000..e8a998f6b8 --- /dev/null +++ b/reports/locations/location_accessions_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + location_accessions_subreport: + identifier: Identifier + title: Title diff --git a/reports/locations/location_accessions_subreport/location_accessions_subreport.erb b/reports/locations/location_accessions_subreport/location_accessions_subreport.erb new file mode 100644 index 0000000000..d7fa95fc34 --- /dev/null +++ b/reports/locations/location_accessions_subreport/location_accessions_subreport.erb @@ -0,0 +1,11 @@ + + + + + <% @report.each do |record| %> + + + + + <% end %> +
      <%= t('identifier') %><%= t('title') %>
      <%= format_4part(record.fetch('identifier')) %><%= transform_text(record.fetch('title')) %>
      diff --git a/reports/locations/location_accessions_subreport/location_accessions_subreport.rb b/reports/locations/location_accessions_subreport/location_accessions_subreport.rb new file mode 100644 index 0000000000..4ce3a7a0dd --- /dev/null +++ b/reports/locations/location_accessions_subreport/location_accessions_subreport.rb @@ -0,0 +1,21 @@ +class LocationAccessionsSubreport < AbstractReport + + def template + "location_accessions_subreport.erb" + end + + def query + db[:location] + .inner_join(:top_container_housed_at_rlshp, :top_container_housed_at_rlshp__id => :location__id) + .inner_join(:top_container, :top_container__id => :top_container_housed_at_rlshp__top_container_id) + .inner_join(:top_container_link_rlshp, :top_container_link_rlshp__top_container_id => :top_container__id) + .inner_join(:sub_container, :sub_container__id => :top_container_link_rlshp__sub_container_id) + .inner_join(:instance, :instance__id => :sub_container__instance_id) + .inner_join(:accession, :accession__id => :instance__accession_id) + .filter(:location__id => @params.fetch(:location_id)) + .select(Sequel.as(:accession__id, :id), + Sequel.as(:accession__identifier, :identifier), + Sequel.as(:accession__title, :title)) + end + +end diff --git a/reports/locations/location_holdings_report/en.yml b/reports/locations/location_holdings_report/en.yml new file mode 100644 index 0000000000..0d1fba7126 --- /dev/null +++ b/reports/locations/location_holdings_report/en.yml @@ -0,0 +1,12 @@ +en: + reports: + location_holdings_report: + title: Location Holdings + description: Report on containers shelved at one or more locations + repository_report_type: Repository + building_report_type: Building + single_location_report_type: Single Location + location_range_report_type: Range of Locations + search_range: Report on a range of locations + start_range: First location + end_range: Last location diff --git a/reports/locations/location_holdings_report/location_holdings_report.rb b/reports/locations/location_holdings_report/location_holdings_report.rb new file mode 100644 index 0000000000..f7cdba7530 --- /dev/null +++ b/reports/locations/location_holdings_report/location_holdings_report.rb @@ -0,0 +1,301 @@ +class LocationHoldingsReport < AbstractReport + + register_report({ + :params => [["locations", "LocationList", "The locations of interest"]] + }) + + include JSONModel + + attr_reader :building, :repository_uri, :start_location, :end_location + + def initialize(params, job, db) + super + + if ASUtils.present?(params['building']) + @building = params['building'] + elsif ASUtils.present?(params['repository_uri']) + @repository_uri = params['repository_uri'] + + RequestContext.open(:repo_id => JSONModel(:repository).id_for(@repository_uri)) do + unless current_user.can?(:view_repository) + raise AccessDeniedException.new("User does not have access to view the requested repository") + end + end + else + @start_location = Location.get_or_die(JSONModel(:location).id_for(params['location_start']['ref'])) + + if ASUtils.present?(params['location_end']) + @end_location = Location.get_or_die(JSONModel(:location).id_for(params['location_end']['ref'])) + end + end + end + + def headers + [ + 'building', 'floor_and_room', 'location_in_room', + 'location_url', 'location_profile', 'location_barcode', + 'resource_or_accession_id', 'resource_or_accession_title', + 'top_container_indicator', 'top_container_barcode', 'container_profile', + 'ils_item_id', 'ils_holding_id', 'repository' + ] + end + + def processor + { + 'floor_and_room' => proc {|row| floor_and_room(row)}, + 'location_in_room' => proc {|row| location_in_room(row)}, + 'location_url' => proc {|row| location_url(row)}, + } + end + + def query + dataset = if building + building_query + elsif repository_uri + repository_query + elsif start_location && end_location + range_query + else + single_query + end + + # Join location to top containers, repository and (optionally) location and container profiles + dataset = dataset + .left_outer_join(:location_profile_rlshp, :location_id => :location__id) + .left_outer_join(:location_profile, :id => :location_profile_rlshp__location_profile_id) + .join(:top_container_housed_at_rlshp, + :location_id => :location__id, + :top_container_housed_at_rlshp__status => 'current') + .join(:top_container, :id => :top_container_housed_at_rlshp__top_container_id) + .join(:repository, :id => :top_container__repo_id) + .left_outer_join(:top_container_profile_rlshp, :top_container_id => :top_container_housed_at_rlshp__top_container_id) + .left_outer_join(:container_profile, :id => :top_container_profile_rlshp__container_profile_id) + + # A top container can be linked (via subcontainer) to an instance attached + # to an archival object, resource or accession. We'd like to report on the + # ultimate collection of that linkage--the accession or resource tree that + # the top container is linked into. + # + # So, here comes more joins... + dataset = dataset + .left_outer_join(:top_container_link_rlshp, :top_container_id => :top_container__id) + .left_outer_join(:sub_container, :id => :top_container_link_rlshp__sub_container_id) + .left_outer_join(:instance, :id => :sub_container__instance_id) + .left_outer_join(:archival_object, :id => :instance__archival_object_id) + .left_outer_join(:resource, :id => :instance__resource_id) + .left_outer_join(:accession, :id => :instance__accession_id) + .left_outer_join(:resource___resource_via_ao, :id => :archival_object__root_record_id) + + # Used so we can combine adjacent rows for accession/resources linkages + # (i.e. one top container linked to multiple collections) + dataset = dataset.order(:top_container_id) + + dataset = dataset.select(Sequel.as(:location__building, :building), + Sequel.as(:location__floor, :floor), + Sequel.as(:location__room, :room), + Sequel.as(:location__area, :area), + Sequel.as(:location__id, :location_id), + + Sequel.as(:location__coordinate_1_label, :coordinate_1_label), + Sequel.as(:location__coordinate_1_indicator, :coordinate_1_indicator), + Sequel.as(:location__coordinate_2_label, :coordinate_2_label), + Sequel.as(:location__coordinate_2_indicator, :coordinate_2_indicator), + Sequel.as(:location__coordinate_3_label, :coordinate_3_label), + Sequel.as(:location__coordinate_3_indicator, :coordinate_3_indicator), + + Sequel.as(:location_profile__name, :location_profile), + Sequel.as(:location__barcode, :location_barcode), + Sequel.as(:top_container__indicator, :top_container_indicator), + Sequel.as(:top_container__barcode, :top_container_barcode), + Sequel.as(:container_profile__name, :container_profile), + Sequel.as(:top_container__id, :top_container_id), + Sequel.as(:top_container__ils_item_id, :ils_item_id), + Sequel.as(:top_container__ils_holding_id, :ils_holding_id), + Sequel.as(:repository__name, :repository), + + Sequel.as(:resource__title, :resource_title), + Sequel.as(:resource_via_ao__title, :resource_via_ao_title), + Sequel.as(:accession__title, :accession_title), + + Sequel.as(:resource__identifier, :resource_identifier), + Sequel.as(:resource_via_ao__identifier, :resource_via_ao_identifier), + Sequel.as(:accession__identifier, :accession_identifier), + ) + + dataset + end + + def building_query + db[:location].filter(:location__building => building) + end + + def repository_query + repo_id = JSONModel.parse_reference(repository_uri)[:id] + + location_ids = db[:location] + .join(:top_container_housed_at_rlshp, :location_id => :location__id) + .join(:top_container, :top_container__id => :top_container_housed_at_rlshp__top_container_id) + .filter(:top_container__repo_id => repo_id) + .select(:location__id) + + ds = db[:location].filter(:location__id => location_ids) + + # We add a filter at this point to only show holdings for the current + # repository. This works because we know our dataset will be joined with + # the top_container table in our `query` method, and Sequel doesn't mind if + # we add filters for columns that haven't been joined in yet. + # + ds.filter(:top_container__repo_id => repo_id) + end + + def single_query + db[:location].filter(:location__id => start_location.id) + end + + def range_query + # Find the most specific mismatch between the two locations: building -> floor -> room -> area -> c1 -> c2 -> c3 + properties_to_compare = [:building, :floor, :room, :area] + + [1, 2, 3].each do |coordinate| + label = "coordinate_#{coordinate}_label" + if !start_location[label].nil? && start_location[label] == end_location[label] + properties_to_compare << "coordinate_#{coordinate}_indicator".intern + else + break + end + end + + matching_properties = [] + determinant_property = nil + + properties_to_compare.each do |property| + + if start_location[property] && end_location[property] + + if start_location[property] == end_location[property] + # If both locations have the same value for this property, we'll skip it for the purposes of our range calculation + matching_properties << property + else + # But if they have different values, that's what we'll use for the basis of our range + determinant_property = property + break + end + + elsif !start_location[property] && !end_location[property] + # If neither location has a value for this property, skip it + next + + else + # If we hit a property that only one location has a value for, we can't use it for a range calculation + break + end + + end + + if matching_properties.empty? && determinant_property.nil? + # an empty dataset + return db[:location].where { 1 == 0 } + end + + dataset = db[:location] + + matching_properties.each do |property| + dataset = dataset.filter(property => start_location[property]) + end + + if determinant_property + range_start, range_end = [start_location[determinant_property], end_location[determinant_property]].sort + dataset = dataset + .filter("#{determinant_property} >= ?", range_start) + .filter("#{determinant_property} <= ?", range_end) + end + + dataset + end + + def each + collection_identifier_fields = [:resource_identifier, :resource_via_ao_identifier, :accession_identifier] + collection_title_fields = [:resource_title, :resource_via_ao_title, :accession_title] + + dataset = query + + current_entry = nil + enum = dataset.to_enum + + while true + row = next_row(enum) + + if row && current_entry && current_entry[:_top_container_id] == row[:top_container_id] + # This row can be combined with the previous entry + collection_identifier_fields.each do |field| + current_entry['resource_or_accession_id'] << row[field] + end + + collection_title_fields.each do |field| + current_entry['resource_or_accession_title'] << row[field] + end + else + if current_entry + # Yield the old value + current_entry.delete(:_top_container_id) + current_entry['resource_or_accession_id'] = current_entry['resource_or_accession_id'].compact.uniq.map {|s| format_identifier(s)}.join('; ') + current_entry['resource_or_accession_title'] = current_entry['resource_or_accession_title'].compact.uniq.join('; ') + yield current_entry + end + + # If we hit the end of our rows, we're all done + break unless row + + # Otherwise, start a new entry for the next row + current_entry = Hash[headers.map { |h| + val = (processor.has_key?(h)) ? processor[h].call(row) : row[h.intern] + [h, val] + }] + + current_entry['resource_or_accession_id'] = collection_identifier_fields.map {|field| row[field]} + current_entry['resource_or_accession_title'] = collection_title_fields.map {|field| row[field]} + + # Use the top container ID to combine adjacent rows + current_entry[:_top_container_id] = row[:top_container_id] + end + end + end + + private + + def next_row(enum) + enum.next + rescue StopIteration + nil + end + + def format_identifier(s) + if ASUtils.blank?(s) + s + else + ASUtils.json_parse(s).compact.join(" -- ") + end + end + + def floor_and_room(row) + [row[:floor], row[:room]].compact.join(', ') + end + + def location_in_room(row) + fields = [row[:area]] + + [1, 2, 3].each do |coordinate| + if row["coordinate_#{coordinate}_label".intern] + fields << ("%s: %s" % [row["coordinate_#{coordinate}_label".intern], + row["coordinate_#{coordinate}_indicator".intern]]) + end + end + + fields.compact.join(', ') + end + + def location_url(row) + JSONModel(:location).uri_for(row[:location_id]) + end + +end diff --git a/reports/locations/location_report/en.yml b/reports/locations/location_report/en.yml new file mode 100644 index 0000000000..82775cba32 --- /dev/null +++ b/reports/locations/location_report/en.yml @@ -0,0 +1,14 @@ +en: + reports: + location_report: + title: Location Report + description: Displays a list of locations, indicating any accessions or resources assigned to defined locations. + total_count: Number of Results + floor: Floor + room: Room + area: Area + coodinates: Coordinates + barcode: Barcode + classificaion_number: Classification number + accessions: Accessions + resources: Resources diff --git a/reports/locations/location_report/location_report.erb b/reports/locations/location_report/location_report.erb new file mode 100644 index 0000000000..af61183678 --- /dev/null +++ b/reports/locations/location_report/location_report.erb @@ -0,0 +1,39 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <% t 'total_count' %>
      +
      <%= h @report.total_count %>
      +
      +
      + +<% @report.each do |location| %> + +
      +
      <%= h location.fetch('location_building') %>
      + +
      +
      <%= t('floor') %>
      +
      <%= format_date(location.fetch('location_floor')) %>
      + +
      <%= t('room') %>
      +
      <%= format_date(location.fetch('location_room')) %>
      + +
      <%= t('area') %>
      +
      <%= format_date(location.fetch('location_area')) %>
      + +
      <%= t('coodinates') %>
      +
      <%= format_date(location.fetch('location_coordinate')) %>
      + +
      <%= t('barcode') %>
      +
      <%= format_date(location.fetch('location_barcode')) %>
      + +
      <%= t('classificaion_number') %>
      +
      <%= format_date(location.fetch('location_classification')) %>
      +
      + + <%= subreport_section(t('accessions'), LocationAccessionsSubreport, location) %> + + <%= subreport_section(t('resources'), LocationResourcesSubreport, location) %> +
      +<% end %> diff --git a/reports/locations/location_report/location_report.rb b/reports/locations/location_report/location_report.rb new file mode 100644 index 0000000000..e576ca753f --- /dev/null +++ b/reports/locations/location_report/location_report.rb @@ -0,0 +1,25 @@ +class LocationReport < AbstractReport + register_report + + def template + "location_report.erb" + end + + def total_count + query.count + end + + def query + db[:location] + .select(Sequel.as(:id, :location_id), + Sequel.as(:building, :location_building), + Sequel.as(:title, :location_title), + Sequel.as(:floor, :location_floor), + Sequel.as(:room, :location_room), + Sequel.as(:area, :location_area), + Sequel.as(:barcode, :location_barcode), + Sequel.as(:classification, :location_classification), + Sequel.as(Sequel.lit("GetCoordinate(id)"), :location_coordinate)) + end + +end diff --git a/reports/locations/location_resources_subreport/en.yml b/reports/locations/location_resources_subreport/en.yml new file mode 100644 index 0000000000..dd61f42fb9 --- /dev/null +++ b/reports/locations/location_resources_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + location_resources_subreport: + identifier: Identifier + title: Title \ No newline at end of file diff --git a/reports/locations/location_resources_subreport/location_resources_subreport.erb b/reports/locations/location_resources_subreport/location_resources_subreport.erb new file mode 100644 index 0000000000..d7fa95fc34 --- /dev/null +++ b/reports/locations/location_resources_subreport/location_resources_subreport.erb @@ -0,0 +1,11 @@ + + + + + <% @report.each do |record| %> + + + + + <% end %> +
      <%= t('identifier') %><%= t('title') %>
      <%= format_4part(record.fetch('identifier')) %><%= transform_text(record.fetch('title')) %>
      diff --git a/reports/locations/location_resources_subreport/location_resources_subreport.rb b/reports/locations/location_resources_subreport/location_resources_subreport.rb new file mode 100644 index 0000000000..3c2ea69ef0 --- /dev/null +++ b/reports/locations/location_resources_subreport/location_resources_subreport.rb @@ -0,0 +1,38 @@ +class LocationResourcesSubreport < AbstractReport + + def template + "location_resources_subreport.erb" + end + + def query + resource_locations = db[:location] + .inner_join(:top_container_housed_at_rlshp, :top_container_housed_at_rlshp__id => :location__id) + .inner_join(:top_container, :top_container__id => :top_container_housed_at_rlshp__top_container_id) + .inner_join(:top_container_link_rlshp, :top_container_link_rlshp__top_container_id => :top_container__id) + .inner_join(:sub_container, :sub_container__id => :top_container_link_rlshp__sub_container_id) + .inner_join(:instance, :instance__id => :sub_container__instance_id) + .inner_join(:resource, :resource__id => :instance__resource_id) + .filter(:location__id => @params.fetch(:location_id)) + .select(Sequel.as(:resource__id, :id), + Sequel.as(:resource__identifier, :identifier), + Sequel.as(:resource__title, :title)) + + archival_object_locations = db[:location] + .inner_join(:top_container_housed_at_rlshp, :top_container_housed_at_rlshp__id => :location__id) + .inner_join(:top_container, :top_container__id => :top_container_housed_at_rlshp__top_container_id) + .inner_join(:top_container_link_rlshp, :top_container_link_rlshp__top_container_id => :top_container__id) + .inner_join(:sub_container, :sub_container__id => :top_container_link_rlshp__sub_container_id) + .inner_join(:instance, :instance__id => :sub_container__instance_id) + .inner_join(:archival_object, :archival_object__id => :instance__archival_object_id) + .inner_join(:resource, :resource__id => :archival_object__root_record_id) + .filter(:location__id => @params.fetch(:location_id)) + .select(Sequel.as(:resource__id, :id), + Sequel.as(:resource__identifier, :identifier), + Sequel.as(:resource__title, :title)) + + + resource_locations + .union(archival_object_locations) + end + +end diff --git a/reports/repositories/repository_report/en.yml b/reports/repositories/repository_report/en.yml new file mode 100644 index 0000000000..bebc94931b --- /dev/null +++ b/reports/repositories/repository_report/en.yml @@ -0,0 +1,5 @@ +en: + reports: + repository_report: + title: Repository Report + description: Report on repository records diff --git a/reports/repositories/repository_report/repository_report.rb b/reports/repositories/repository_report/repository_report.rb new file mode 100644 index 0000000000..4b14ee62be --- /dev/null +++ b/reports/repositories/repository_report/repository_report.rb @@ -0,0 +1,18 @@ +class RepositoryReport < AbstractReport + register_report + + def headers + Repository.columns + end + + def processor + { + 'identifier' => proc {|record| ASUtils.json_parse(record[:identifier] || "[]").compact.join("-")}, + } + end + + def query + db[:repository].filter(:hidden => 0) + end + +end diff --git a/reports/resources/resource_deaccessions_list_report/en.yml b/reports/resources/resource_deaccessions_list_report/en.yml new file mode 100644 index 0000000000..d5cc2db689 --- /dev/null +++ b/reports/resources/resource_deaccessions_list_report/en.yml @@ -0,0 +1,13 @@ +en: + reports: + resource_deaccessions_list_report: + title: Resources and Deaccession(s) + description: Displays resource(s) and linked deaccession record(s). Report contains title, resource identifier, level, date range, linked deaccessions, creator names, and physical extent totals. + total_count: Number of Records + total_extent: Total Extent of Resources + total_extent_of_deaccessions: Total Extent of Deaccessions + identifier_prefix: Resource + date: Date + level: Level + deaccessions: Deaccessions + diff --git a/reports/resources/resource_deaccessions_list_report/resource_deaccessions_list_report.erb b/reports/resources/resource_deaccessions_list_report/resource_deaccessions_list_report.erb new file mode 100644 index 0000000000..741181c05b --- /dev/null +++ b/reports/resources/resource_deaccessions_list_report/resource_deaccessions_list_report.erb @@ -0,0 +1,28 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      +
      <%= t('total_extent_of_deaccessions') %>
      +
      <%= format_number(@report.total_extent_of_deaccessions) %>
      +
      +
      + +<% @report.each do |resource| %> +
      +
      <%= t("identifier_prefix") %> <%= format_4part(resource.fetch('resourceIdentifier')) %>
      +
      <%= h resource.fetch('title') %>
      + +
      +
      <%= t('date') %>
      +
      <%= h resource.fetch('dateExpression') %>
      +
      <%= t('level') %>
      +
      <%= h resource.fetch('level') %>
      +
      + + <%= subreport_section(t('deaccessions'), ResourceDeaccessionsSubreport, resource) %> +
      +<% end %> diff --git a/reports/resources/resource_deaccessions_list_report/resource_deaccessions_list_report.rb b/reports/resources/resource_deaccessions_list_report/resource_deaccessions_list_report.rb new file mode 100644 index 0000000000..2e04e256ee --- /dev/null +++ b/reports/resources/resource_deaccessions_list_report/resource_deaccessions_list_report.rb @@ -0,0 +1,44 @@ +class ResourceDeaccessionsListReport < AbstractReport + + register_report + + def template + 'resource_deaccessions_list_report.erb' + end + + def query + db[:resource]. + filter(Sequel.lit('GetResourceHasDeaccession(id)') => 1). + select(Sequel.as(:id, :resourceId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:title, :title), + Sequel.as(:identifier, :resourceIdentifier), + Sequel.as(Sequel.lit('GetEnumValueUF(level_id)'), :level), + Sequel.as(Sequel.lit('GetResourceDateExpression(id)'), :dateExpression), + Sequel.as(Sequel.lit('GetResourceExtent(id)'), :extentNumber), + Sequel.as(Sequel.lit('GetResourceDeaccessionExtent(id)'), :deaccessionExtentNumber)) + end + + # Number of Records + def total_count + @total_count ||= self.query.count + end + + # Total Extent of Resources + def total_extent + @total_extent ||= db.from(self.query).sum(:extentNumber) + end + + # Total Extent of Deaccessions + def total_extent_of_deaccessions + return @total_extent_of_deaccessions if @total_extent_of_deaccessions + + deaccessions = db[:deaccession].where(:accession_id => self.query.select(:id)) + deaccession_extents = db[:extent].where(:deaccession_id => deaccessions.select(:id)) + + @total_extent_of_deaccessions = deaccession_extents.sum(:number) + + @total_extent_of_deaccessions + end + +end diff --git a/reports/resources/resource_deaccessions_subreport/en.yml b/reports/resources/resource_deaccessions_subreport/en.yml new file mode 100644 index 0000000000..7ae0828394 --- /dev/null +++ b/reports/resources/resource_deaccessions_subreport/en.yml @@ -0,0 +1,7 @@ +en: + reports: + resource_deaccessions_subreport: + description: Description + extent: Extent + date: Date + notification_sent: Notification Sent diff --git a/reports/resources/resource_deaccessions_subreport/resource_deaccessions_subreport.erb b/reports/resources/resource_deaccessions_subreport/resource_deaccessions_subreport.erb new file mode 100644 index 0000000000..425d5d5937 --- /dev/null +++ b/reports/resources/resource_deaccessions_subreport/resource_deaccessions_subreport.erb @@ -0,0 +1,15 @@ + + + + + + + <% @report.each do |deaccession| %> + + + + + + + <% end %> +
      <%= t('description') %><%= t('extent') %><%= t('date') %><%= t('notification_sent') %>
      <%= h deaccession.fetch('description') %><%= format_number(deaccession.fetch('extentNumber')) %> <%= deaccession.fetch('extentType') %><%= format_date(deaccession.fetch('deaccessionDate')) %><%= format_boolean(deaccession.fetch('notification')) %>
      diff --git a/reports/resources/resource_deaccessions_subreport/resource_deaccessions_subreport.rb b/reports/resources/resource_deaccessions_subreport/resource_deaccessions_subreport.rb new file mode 100644 index 0000000000..c7dab4afd1 --- /dev/null +++ b/reports/resources/resource_deaccessions_subreport/resource_deaccessions_subreport.rb @@ -0,0 +1,22 @@ +class ResourceDeaccessionsSubreport < AbstractReport + + def template + "resource_deaccessions_subreport.erb" + end + + def accession_count + query.count + end + + def query + db[:deaccession] + .filter(:resource_id => @params.fetch(:resourceId)) + .select(Sequel.as(:id, :deaccessionId), + Sequel.as(:description, :description), + Sequel.as(:notification, :notification), + Sequel.as(Sequel.lit("GetDeaccessionDate(id)"), :deaccessionDate), + Sequel.as(Sequel.lit("GetDeaccessionExtent(id)"), :extentNumber), + Sequel.as(Sequel.lit("GetDeaccessionExtentType(id)"), :extentType)) + end + +end diff --git a/reports/resources/resource_instances_list_report/en.yml b/reports/resources/resource_instances_list_report/en.yml new file mode 100644 index 0000000000..8f0594e1ce --- /dev/null +++ b/reports/resources/resource_instances_list_report/en.yml @@ -0,0 +1,12 @@ +en: + reports: + resource_instances_list_report: + title: Resources and Instances List + description: Displays resource(s) and all specified location information. Report contains title, resource identifier, level, date range, and assigned containers. + total_count: Number of Records + total_extent: Total Extent of Resources + identifier_prefix: Resource + date: Date + level: Level + containers: Containers + diff --git a/reports/resources/resource_instances_list_report/resource_instances_list_report.erb b/reports/resources/resource_instances_list_report/resource_instances_list_report.erb new file mode 100644 index 0000000000..266cf8e55a --- /dev/null +++ b/reports/resources/resource_instances_list_report/resource_instances_list_report.erb @@ -0,0 +1,26 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      +
      +
      + +<% @report.each do |resource| %> +
      +
      <%= t('identifier_prefix') %> <%= format_4part(resource.fetch('resourceIdentifier')) %>
      +
      <%= h resource.fetch('title') %>
      + +
      +
      <%= t('date') %>
      +
      <%= h resource.fetch('dateExpression') %>
      +
      <%= t('level') %>
      +
      <%= h resource.fetch('level') %>
      +
      + + <%= subreport_section(t('containers'), ResourceInstancesSubreport, resource) %> +
      +<% end %> diff --git a/reports/resources/resource_instances_list_report/resource_instances_list_report.rb b/reports/resources/resource_instances_list_report/resource_instances_list_report.rb new file mode 100644 index 0000000000..47e23466d0 --- /dev/null +++ b/reports/resources/resource_instances_list_report/resource_instances_list_report.rb @@ -0,0 +1,30 @@ +class ResourceInstancesListReport < AbstractReport + + register_report + + def template + 'resource_instances_list_report.erb' + end + + def query + db[:resource]. + select(Sequel.as(:id, :resourceId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:title, :title), + Sequel.as(:identifier, :resourceIdentifier), + Sequel.as(Sequel.lit('GetEnumValueUF(level_id)'), :level), + Sequel.as(Sequel.lit('GetResourceDateExpression(id)'), :dateExpression), + Sequel.as(Sequel.lit('GetResourceExtent(id)'), :extentNumber)) + end + + # Number of Records + def total_count + @total_count ||= self.query.count + end + + # Total Extent of Resources + def total_extent + @total_extent ||= db.from(self.query).sum(:extentNumber) + end + +end diff --git a/reports/resources/resource_instances_subreport/en.yml b/reports/resources/resource_instances_subreport/en.yml new file mode 100644 index 0000000000..758952fd08 --- /dev/null +++ b/reports/resources/resource_instances_subreport/en.yml @@ -0,0 +1,6 @@ +en: + reports: + resource_instances_subreport: + top_container: Top Container + container_2: Container 2 + container_3: Container 3 \ No newline at end of file diff --git a/reports/resources/resource_instances_subreport/resource_instances_subreport.erb b/reports/resources/resource_instances_subreport/resource_instances_subreport.erb new file mode 100644 index 0000000000..b916bc1623 --- /dev/null +++ b/reports/resources/resource_instances_subreport/resource_instances_subreport.erb @@ -0,0 +1,15 @@ + + + + + + + + <% @report.each do |location| %> + + + + + + <% end %> +
      <%= t('top_container') %><%= t('container_2') %><%= t('container_3') %>
      <%= h location.fetch('container') %><%= h location.fetch('container2Type') %> <%= h location.fetch('container2Indicator') %><%= h location.fetch('container3Type') %> <%= h location.fetch('container3Indicator') %>
      diff --git a/reports/resources/resource_instances_subreport/resource_instances_subreport.rb b/reports/resources/resource_instances_subreport/resource_instances_subreport.rb new file mode 100644 index 0000000000..5bd612fa2b --- /dev/null +++ b/reports/resources/resource_instances_subreport/resource_instances_subreport.rb @@ -0,0 +1,32 @@ +class ResourceInstancesSubreport < AbstractReport + + def template + 'resource_instances_subreport.erb' + end + + # FIXME might be nice to group the containers by their top container? + # They are currently listed one per row (see hornstein) + def query + resource_id = @params.fetch(:resourceId) + all_children_ids = db[:archival_object] + .filter(:root_record_id => resource_id) + .select(:id) + db[:instance] + .inner_join(:sub_container, :instance_id => :instance__id) + .inner_join(:top_container_link_rlshp, :sub_container_id => :sub_container__id) + .inner_join(:top_container, :id => :top_container_link_rlshp__top_container_id) + .left_outer_join(:top_container_profile_rlshp, :top_container_id => :top_container__id) + .left_outer_join(:container_profile, :id => :top_container_profile_rlshp__container_profile_id) + .filter { + Sequel.|({:instance__resource_id => resource_id}, + :instance__archival_object_id => all_children_ids) + } + .select(Sequel.as(Sequel.lit("CONCAT(COALESCE(container_profile.name, ''), ' ', top_container.indicator)"), :container), + Sequel.as(Sequel.lit("GetEnumValueUF(sub_container.type_2_id)"), :container2Type), + Sequel.as(:sub_container__indicator_2, :container2Indicator), + Sequel.as(Sequel.lit("GetEnumValueUF(sub_container.type_3_id)"), :container3Type), + Sequel.as(:sub_container__indicator_3, :container3Indicator)) + .distinct + end + +end diff --git a/reports/resources/resource_locations_list_report/en.yml b/reports/resources/resource_locations_list_report/en.yml new file mode 100644 index 0000000000..4d8b882103 --- /dev/null +++ b/reports/resources/resource_locations_list_report/en.yml @@ -0,0 +1,11 @@ +en: + reports: + resource_locations_list_report: + title: Resources and Locations List + description: Displays resource(s) and all specified location information. Report contains title, resource identifier, level, date range, and assigned locations. + total_count: Number of Records + total_extent: Total Extent of Resources + identifier_prefix: Resource + dates: Dates + level: Level + containers: Containers diff --git a/reports/resources/resource_locations_list_report/resource_locations_list_report.erb b/reports/resources/resource_locations_list_report/resource_locations_list_report.erb new file mode 100644 index 0000000000..f5c46515be --- /dev/null +++ b/reports/resources/resource_locations_list_report/resource_locations_list_report.erb @@ -0,0 +1,26 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count')%>
      +
      <%= h @report.total_count %>
      +
      <%= t('total_extent')%>
      +
      <%= format_number(@report.total_extent) %>
      +
      +
      + +<% @report.each do |resource| %> +
      +
      <%= t('identifier_prefix') %> <%= format_4part(resource.fetch('resourceIdentifier')) %>
      +
      <%= h resource.fetch('title') %>
      + +
      +
      <%= t('dates') %>
      +
      <%= h resource.fetch('dateExpression') %>
      +
      <%= t('level') %>
      +
      <%= h resource.fetch('level') %>
      +
      + + <%= subreport_section(t('containers'), ResourceLocationsSubreport, resource) %> +
      +<% end %> diff --git a/reports/resources/resource_locations_list_report/resource_locations_list_report.rb b/reports/resources/resource_locations_list_report/resource_locations_list_report.rb new file mode 100644 index 0000000000..c47fed3f6f --- /dev/null +++ b/reports/resources/resource_locations_list_report/resource_locations_list_report.rb @@ -0,0 +1,30 @@ +class ResourceLocationsListReport < AbstractReport + + register_report + + def template + 'resource_locations_list_report.erb' + end + + def query + db[:resource]. + select(Sequel.as(:id, :resourceId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:title, :title), + Sequel.as(:identifier, :resourceIdentifier), + Sequel.as(Sequel.lit('GetEnumValueUF(level_id)'), :level), + Sequel.as(Sequel.lit('GetResourceDateExpression(id)'), :dateExpression), + Sequel.as(Sequel.lit('GetResourceExtent(id)'), :extentNumber)) + end + + # Number of Records + def total_count + @total_count ||= self.query.count + end + + # Total Extent of Resources + def total_extent + @total_extent ||= db.from(self.query).sum(:extentNumber) + end + +end diff --git a/reports/resources/resource_locations_subreport/en.yml b/reports/resources/resource_locations_subreport/en.yml new file mode 100644 index 0000000000..0ad5022702 --- /dev/null +++ b/reports/resources/resource_locations_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + resource_locations_subreport: + location: Location + container: Container \ No newline at end of file diff --git a/reports/resources/resource_locations_subreport/resource_locations_subreport.erb b/reports/resources/resource_locations_subreport/resource_locations_subreport.erb new file mode 100644 index 0000000000..5f162df460 --- /dev/null +++ b/reports/resources/resource_locations_subreport/resource_locations_subreport.erb @@ -0,0 +1,13 @@ + + + + + + + <% @report.each do |location| %> + + + + + <% end %> +
      <%= t('location') %><%= t('container') %>
      <%= h location.fetch('location') %><%= h location.fetch('container') %>
      diff --git a/reports/resources/resource_locations_subreport/resource_locations_subreport.rb b/reports/resources/resource_locations_subreport/resource_locations_subreport.rb new file mode 100644 index 0000000000..00f5728c01 --- /dev/null +++ b/reports/resources/resource_locations_subreport/resource_locations_subreport.rb @@ -0,0 +1,29 @@ +class ResourceLocationsSubreport < AbstractReport + + def template + 'resource_locations_subreport.erb' + end + + def query + resource_id = @params.fetch(:resourceId) + all_children_ids = db[:archival_object] + .filter(:root_record_id => resource_id) + .select(:id) + db[:instance] + .inner_join(:sub_container, :instance_id => :instance__id) + .inner_join(:top_container_link_rlshp, :sub_container_id => :sub_container__id) + .inner_join(:top_container, :id => :top_container_link_rlshp__top_container_id) + .left_outer_join(:top_container_profile_rlshp, :top_container_id => :top_container__id) + .left_outer_join(:container_profile, :id => :top_container_profile_rlshp__container_profile_id) + .inner_join(:top_container_housed_at_rlshp, :top_container_id => :top_container__id) + .inner_join(:location, :id => :top_container_housed_at_rlshp__location_id) + .group_by(:location__id) + .filter { + Sequel.|({:instance__resource_id => resource_id}, + :instance__archival_object_id => all_children_ids) + } + .select(Sequel.as(:location__title, :location), + Sequel.as(Sequel.lit("GROUP_CONCAT(CONCAT(COALESCE(container_profile.name, ''), ' ', top_container.indicator) SEPARATOR ', ')"), :container)) + end + +end diff --git a/reports/resources/resource_names_as_creators_subreport/en.yml b/reports/resources/resource_names_as_creators_subreport/en.yml new file mode 100644 index 0000000000..0908535407 --- /dev/null +++ b/reports/resources/resource_names_as_creators_subreport/en.yml @@ -0,0 +1,5 @@ +en: + reports: + resource_names_as_creators_subreport: + name: Name + relator: Relator \ No newline at end of file diff --git a/reports/resources/resource_names_as_creators_subreport/resource_names_as_creators_subreport.erb b/reports/resources/resource_names_as_creators_subreport/resource_names_as_creators_subreport.erb new file mode 100644 index 0000000000..35391bf8a0 --- /dev/null +++ b/reports/resources/resource_names_as_creators_subreport/resource_names_as_creators_subreport.erb @@ -0,0 +1,13 @@ + + + + + + + <% @report.each do |creator| %> + + + + + <% end %> +
      <%= t('name') %><%= t('relator') %>
      <%= h creator.fetch('sortName') %><%= h creator.fetch('relator') %>
      diff --git a/reports/resources/resource_names_as_creators_subreport/resource_names_as_creators_subreport.rb b/reports/resources/resource_names_as_creators_subreport/resource_names_as_creators_subreport.rb new file mode 100644 index 0000000000..e4e0125bd9 --- /dev/null +++ b/reports/resources/resource_names_as_creators_subreport/resource_names_as_creators_subreport.rb @@ -0,0 +1,32 @@ +class ResourceNamesAsCreatorsSubreport < AbstractReport + + def template + 'resource_names_as_creators_subreport.erb' + end + + # FIXME I81n relator + def query + resource_id = @params.fetch(:resourceId) + all_children_ids = db[:archival_object] + .filter(:root_record_id => resource_id) + .select(:id) + + creator_id = db[:enumeration_value] + .filter(:enumeration_id => db[:enumeration].filter(:name => 'linked_agent_role').select(:id)) + .filter(:value => 'creator') + .select(:id) + + + dataset = db[:linked_agents_rlshp] + .filter(:role_id => creator_id) + .filter { + Sequel.|({:resource_id => resource_id}, + :archival_object_id => all_children_ids) + } + .select(Sequel.as(Sequel.lit("GetAgentSortname(agent_person_id, agent_family_id, agent_corporate_entity_id)"), :sortName), + Sequel.as(Sequel.lit("GetEnumValue(relator_id)"), :relator)) + + db.from(dataset).group(:sortName) + end + +end diff --git a/reports/resources/resource_restrictions_list_report/en.yml b/reports/resources/resource_restrictions_list_report/en.yml new file mode 100644 index 0000000000..de2a7d49f5 --- /dev/null +++ b/reports/resources/resource_restrictions_list_report/en.yml @@ -0,0 +1,16 @@ +en: + reports: + resource_restrictions_list_report: + title: Restricted Resources + description: Displays only those resource(s) that are restricted. Report contains title, resource identifier, level, date range, creator names, and a total extent number of the records selected that are checked as restrictions apply. + total_count: Number of Records + restricted_count: Number of Restricted Records + total_extent: Total Extent of Resources + total_restricted_extent: Total Extent of Restricted Resources + identifier_prefix: Resource + dates: Dates + level: Level + access: Access + restrictions_apply_true: Restrictions apply. + restrictions_apply_false: Open to researchers without restrictions. + names_as_creators: Names as Creators diff --git a/reports/resources/resource_restrictions_list_report/resource_restrictions_list_report.erb b/reports/resources/resource_restrictions_list_report/resource_restrictions_list_report.erb new file mode 100644 index 0000000000..24a6a1e43f --- /dev/null +++ b/reports/resources/resource_restrictions_list_report/resource_restrictions_list_report.erb @@ -0,0 +1,39 @@ +
      +
      <%= h @report.title %>
      + +
      +
      <%= t('total_count') %>
      +
      <%= h @report.total_count %>
      +
      <%= t('restricted_count') %>
      +
      <%= h @report.restricted_count %>
      +
      <%= t('total_extent') %>
      +
      <%= format_number(@report.total_extent) %>
      +
      <%= t('total_restricted_extent') %>
      +
      <%= format_number(@report.total_restricted_extent) %>
      +
      +
      + +<% @report.each do |resource| %> + <% next if resource.fetch('restrictionsApply') != 1 %> + +
      <%= format_4part(resource.fetch('resourceIdentifier')) %>
      +
      <%= h resource.fetch('title') %>
      + +
      +
      <%= t('dates') %>
      +
      <%= h resource.fetch('dateExpression') %>
      +
      <%= t('level') %>
      +
      <%= h resource.fetch('level') %>
      +
      <%= t('access') %>
      +
      + <% if resource.fetch('restrictionsApply') == 1 %> + <%= t('restrictions_apply_true') %> + <% else %> + <%= t('restrictions_apply_false') %> + <% end %> +
      +
      + + <%= subreport_section(t('names_as_creators'), ResourceNamesAsCreatorsSubreport, resource) %> +
      +<% end %> diff --git a/reports/resources/resource_restrictions_list_report/resource_restrictions_list_report.rb b/reports/resources/resource_restrictions_list_report/resource_restrictions_list_report.rb new file mode 100644 index 0000000000..11d1b839a6 --- /dev/null +++ b/reports/resources/resource_restrictions_list_report/resource_restrictions_list_report.rb @@ -0,0 +1,44 @@ +class ResourceRestrictionsListReport < AbstractReport + + register_report + + def template + 'resource_restrictions_list_report.erb' + end + + def query + db[:resource]. + select(Sequel.as(:id, :resourceId), + Sequel.as(:repo_id, :repo_id), + Sequel.as(:title, :title), + Sequel.as(:identifier, :resourceIdentifier), + Sequel.as(:restrictions, :restrictionsApply), + Sequel.as(Sequel.lit('GetEnumValueUF(level_id)'), :level), + Sequel.as(Sequel.lit('GetResourceDateExpression(id)'), :dateExpression), + Sequel.as(Sequel.lit('GetResourceExtent(id)'), :extentNumber)) + end + + # Number of Records + def total_count + @total_count ||= self.query.count + end + + def restricted_count + @restricted_count ||= db.from(self.query) + .filter(:restrictionsApply => 1) + .count + end + + # Total Extent of Resources + def total_extent + @total_extent ||= db.from(self.query).sum(:extentNumber) + end + + # Total Extent of Restricted Resources + def total_restricted_extent + @total_restricted_extent ||= db.from(self.query) + .filter(:restrictionsApply => 1) + .sum(:extentNumber) + end + +end diff --git a/reports/static/fonts/dejavu/DejaVuSans.ttf b/reports/static/fonts/dejavu/DejaVuSans.ttf deleted file mode 100644 index 27cff476ef..0000000000 Binary files a/reports/static/fonts/dejavu/DejaVuSans.ttf and /dev/null differ diff --git a/reports/static/fonts/dejavu/LICENSE b/reports/static/fonts/dejavu/LICENSE deleted file mode 100644 index 254e2cc42a..0000000000 --- a/reports/static/fonts/dejavu/LICENSE +++ /dev/null @@ -1,99 +0,0 @@ -Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. -Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) - -Bitstream Vera Fonts Copyright ------------------------------- - -Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is -a trademark of Bitstream, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of the fonts accompanying this license ("Fonts") and associated -documentation files (the "Font Software"), to reproduce and distribute the -Font Software, including without limitation the rights to use, copy, merge, -publish, distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to the -following conditions: - -The above copyright and trademark notices and this permission notice shall -be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular -the designs of glyphs or characters in the Fonts may be modified and -additional glyphs or characters may be added to the Fonts, only if the fonts -are renamed to names not containing either the words "Bitstream" or the word -"Vera". - -This License becomes null and void to the extent applicable to Fonts or Font -Software that has been modified and is distributed under the "Bitstream -Vera" names. - -The Font Software may be sold as part of a larger software package but no -copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, -TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME -FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING -ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE -FONT SOFTWARE. - -Except as contained in this notice, the names of Gnome, the Gnome -Foundation, and Bitstream Inc., shall not be used in advertising or -otherwise to promote the sale, use or other dealings in this Font Software -without prior written authorization from the Gnome Foundation or Bitstream -Inc., respectively. For further information, contact: fonts at gnome dot -org. - -Arev Fonts Copyright ------------------------------- - -Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of the fonts accompanying this license ("Fonts") and -associated documentation files (the "Font Software"), to reproduce -and distribute the modifications to the Bitstream Vera Font Software, -including without limitation the rights to use, copy, merge, publish, -distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to -the following conditions: - -The above copyright and trademark notices and this permission notice -shall be included in all copies of one or more of the Font Software -typefaces. - -The Font Software may be modified, altered, or added to, and in -particular the designs of glyphs or characters in the Fonts may be -modified and additional glyphs or characters may be added to the -Fonts, only if the fonts are renamed to names not containing either -the words "Tavmjong Bah" or the word "Arev". - -This License becomes null and void to the extent applicable to Fonts -or Font Software that has been modified and is distributed under the -"Tavmjong Bah Arev" names. - -The Font Software may be sold as part of a larger software package but -no copy of one or more of the Font Software typefaces may be sold by -itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL -TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. - -Except as contained in this notice, the name of Tavmjong Bah shall not -be used in advertising or otherwise to promote the sale, use or other -dealings in this Font Software without prior written authorization -from Tavmjong Bah. For further information, contact: tavmjong @ free -. fr. - -$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ diff --git a/reports/static/images/archivesspace.original.png b/reports/static/images/archivesspace.original.png deleted file mode 100644 index 1e9808e558..0000000000 Binary files a/reports/static/images/archivesspace.original.png and /dev/null differ diff --git a/reports/static/images/archivesspace.small.png b/reports/static/images/archivesspace.small.png deleted file mode 100644 index 47345a8796..0000000000 Binary files a/reports/static/images/archivesspace.small.png and /dev/null differ diff --git a/reports/static/images/archivesspace.thumnail.png b/reports/static/images/archivesspace.thumnail.png deleted file mode 100644 index a6cb373135..0000000000 Binary files a/reports/static/images/archivesspace.thumnail.png and /dev/null differ diff --git a/reports/subjects/subject_list_report/en.yml b/reports/subjects/subject_list_report/en.yml new file mode 100644 index 0000000000..d4b730ff1b --- /dev/null +++ b/reports/subjects/subject_list_report/en.yml @@ -0,0 +1,6 @@ +en: + reports: + subject_list_report: + title: Subject Records List + description: Displays selected subject record(s). Report lists subject term, subject term type, and subject source. + diff --git a/reports/subjects/subject_list_report/subject_list_report.rb b/reports/subjects/subject_list_report/subject_list_report.rb new file mode 100644 index 0000000000..32491a036c --- /dev/null +++ b/reports/subjects/subject_list_report/subject_list_report.rb @@ -0,0 +1,22 @@ +class SubjectListReport < AbstractReport + + register_report + + def template + 'generic_listing.erb' + end + + def headers + ['subject_title', 'subject_term_type', 'subject_source'] + end + + def query + db[:subject] + .join(:enumeration_value, :id => :source_id) + .select(Sequel.as(:subject__id, :subject_id), + Sequel.as(:subject__title, :subject_title), + Sequel.as(:subject__source_id, :subject_source_id), + Sequel.as(Sequel.lit('GetTermType(subject.id)'), :subject_term_type), + Sequel.as(:enumeration_value__value, :subject_source)) + end +end diff --git a/scripts/jruby b/scripts/jruby index 7f549119bc..c31ddc9b8f 100755 --- a/scripts/jruby +++ b/scripts/jruby @@ -8,4 +8,4 @@ basedir="`dirname $0`/../" export GEM_HOME="`(cd "$basedir/build/gems"; pwd)`" export GEM_PATH="" -exec java $JAVA_OPTS -cp "$basedir/build/*:$basedir/common/lib/*" org.jruby.Main --1.9 ${1+"$@"} +exec java $JAVA_OPTS -cp "$basedir/build/*:$basedir/common/lib/*" org.jruby.Main ${1+"$@"} diff --git a/selenium-public/Gemfile b/selenium-public/Gemfile index d39026967f..fd729a7ec7 100644 --- a/selenium-public/Gemfile +++ b/selenium-public/Gemfile @@ -1,18 +1,20 @@ # A sample Gemfile source "https://rubygems.org" +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + group :test do - gem "rack", "~> 1.4.7" - gem "activesupport", "~> 3.2.22" - gem "selenium-webdriver", "~> 2.53.0" - gem "rspec", "~> 3.3.0" + gem "rack" + gem "activesupport", "5.0.1" + gem "selenium-webdriver", "= 3.3.0" + gem "rspec" gem "rspec-retry" gem "json-schema", "1.0.10" gem "atomic", '= 1.0.1' gem "net-http-persistent", "2.8" gem "multipart-post", "1.2.0" - gem "json", "1.8.0" gem 'multi_json', '~> 1.12.1' diff --git a/selenium-public/Gemfile.lock b/selenium-public/Gemfile.lock index 0d118e95d8..7c26de5269 100644 --- a/selenium-public/Gemfile.lock +++ b/selenium-public/Gemfile.lock @@ -1,42 +1,51 @@ GEM remote: https://rubygems.org/ specs: - activesupport (3.2.22.2) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) atomic (1.0.1-java) - childprocess (0.5.9) + childprocess (0.6.2) ffi (~> 1.0, >= 1.0.11) - diff-lcs (1.2.5) - ffi (1.9.13) + concurrent-ruby (1.0.4-java) + diff-lcs (1.3) + ffi (1.9.18-java) i18n (0.7.0) json (1.8.0-java) json-schema (1.0.10) + minitest (5.10.1) multi_json (1.12.1) multipart-post (1.2.0) net-http-persistent (2.8) - rack (1.4.7) - rspec (3.3.0) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + rack (2.0.1) + rspec (3.5.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-retry (0.4.5) - rspec-core - rspec-support (3.3.0) + rspec-support (~> 3.5.0) + rspec-retry (0.5.3) + rspec-core (> 3.3, < 3.6) + rspec-support (3.5.0) rubyzip (1.0.0) - selenium-webdriver (2.53.4) + selenium-webdriver (3.3.0) childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) - websocket (1.2.3) + thread_safe (0.3.5-java) + tzinfo (1.2.2) + thread_safe (~> 0.1) + tzinfo-data (1.2017.1) + tzinfo (>= 1.0.0) + websocket (1.2.4) zip-zip (0.3) rubyzip (>= 1.0.0) @@ -44,18 +53,19 @@ PLATFORMS java DEPENDENCIES - activesupport (~> 3.2.22) + activesupport (= 5.0.1) atomic (= 1.0.1) json (= 1.8.0) json-schema (= 1.0.10) multi_json (~> 1.12.1) multipart-post (= 1.2.0) net-http-persistent (= 2.8) - rack (~> 1.4.7) - rspec (~> 3.3.0) + rack + rspec rspec-retry rubyzip (= 1.0.0) - selenium-webdriver (~> 2.53.0) + selenium-webdriver (= 3.3.0) + tzinfo-data zip-zip (= 0.3) BUNDLED WITH diff --git a/selenium-public/scripts/selenium-irb.sh b/selenium-public/scripts/selenium-irb.sh index 29dd2eec3a..1cc044fd33 100755 --- a/selenium-public/scripts/selenium-irb.sh +++ b/selenium-public/scripts/selenium-irb.sh @@ -6,7 +6,7 @@ rlwrap="`which rlwrap 2>/dev/null`" -GEM_HOME=$PWD/../../build/gems $rlwrap java -cp ../../build/jruby-complete-*.jar org.jruby.Main --1.9 \ +GEM_HOME=$PWD/../../build/gems $rlwrap java -cp ../../build/jruby-complete-*.jar org.jruby.Main \ -I "../spec/" -r spec_helper.rb -r irb -e ' selenium_init diff --git a/selenium-public/spec/selenium_spec.rb b/selenium-public/spec/selenium_spec.rb index df94985179..6ea274440a 100644 --- a/selenium-public/spec/selenium_spec.rb +++ b/selenium-public/spec/selenium_spec.rb @@ -2,28 +2,8 @@ describe "ArchivesSpace Public interface" do - # Start the dev servers and Selenium before(:all) do - state = Object.new.instance_eval do - @store = {} - - def get_last_mtime(repo_id, record_type) - @store[[repo_id, record_type]].to_i || 0 - end - - def set_last_mtime(repo_id, record_type, time) - @store[[repo_id, record_type]] = time - end - - self - end - - @indexer = PeriodicIndexer.get_indexer(state) - end - - - before(:all) do - @repo = create(:repo) + @repo = create(:repo, :publish => true) set_repo(@repo) @driver = Driver.new.go_home @@ -43,7 +23,7 @@ def self.xdescribe(*stuff) after(:each) do |example| if example.exception and ENV['SCREENSHOT_ON_ERROR'] - SeleniumTest.save_screenshot + SeleniumTest.save_screenshot(@driver) end end @@ -67,23 +47,25 @@ def self.xdescribe(*stuff) create(:repo, :repo_code => @test_repo_code_1, - :name => @test_repo_name_1) + :name => @test_repo_name_1, + :publish => true) create(:repo, :repo_code => @test_repo_code_2, - :name => @test_repo_name_2) + :name => @test_repo_name_2, + :publish => true) - @indexer.run_index_round + run_index_round end it "lists all available repositories" do - @driver.find_element(:link, "Repositories").click + @driver.click_and_wait_until_gone(:link, "Repositories") @driver.find_element_with_text('//a', /#{@test_repo_code_1}/) @driver.find_element_with_text('//a', /#{@test_repo_code_2}/) end it "shows Title (default) in the sort pulldown" do - @driver.find_element(:link, "Repositories").click + @driver.click_and_wait_until_gone(:link, "Repositories") @driver.find_element(:xpath, "//a/span[ text() = 'Title Ascending']").click @driver.find_element(:link, "Title" ) @driver.ensure_no_such_element(:link, "Term") @@ -114,13 +96,13 @@ def self.xdescribe(*stuff) :title => "Unpublished Resource", :publish => false, :id_0 => "unpublished") @published = create(:resource, :title => "Published Resource", :publish => true, :id_0 => "published", :notes => notes) + + run_index_round end it "doesn't list an un-published records in the list" do - @indexer.run_index_round - - @driver.find_element(:link, "Collections").click + @driver.click_and_wait_until_gone(:link, "Collections") @driver.find_element(:link, @published.title) @driver.ensure_no_such_element(:link, @unpublished.title) @@ -154,11 +136,11 @@ def self.xdescribe(*stuff) :title => "NO WAY", :id_0 => rand(1000).to_s, :finding_aid_filing_title => "YeaBuddy", :publish => true ) - @indexer.run_index_round + run_index_round - @driver.find_element(:link, "Collections").click + @driver.click_and_wait_until_gone(:link, "Collections") - @driver.find_element(:link, "YeaBuddy").click + @driver.click_and_wait_until_gone(:link, "YeaBuddy") @driver.find_element_with_text('//li', /YeaBuddy/ ) @driver.find_element_with_text('//h2', /YeaBuddy/ ) @@ -171,17 +153,17 @@ def self.xdescribe(*stuff) :title => "Test Resource #{i}", :publish => true, :id_0 => "id#{i}") end - @indexer.run_index_round + run_index_round - @driver.find_element(:link, "Collections").click + @driver.click_and_wait_until_gone(:link, "Collections") - @driver.find_element(:css, '.pagination .active a').text.should eq('1') + expect(@driver.find_element(:css, '.pagination .active a').text).to eq('1') - @driver.find_element(:link, '2').click - @driver.find_element(:css, '.pagination .active a').text.should eq('2') + @driver.click_and_wait_until_gone(:link, '2') + expect(@driver.find_element(:css, '.pagination .active a').text).to eq('2') - @driver.find_element(:link, '1').click - @driver.find_element(:css, '.pagination .active a').text.should eq('1') + @driver.click_and_wait_until_gone(:link, '1') + expect(@driver.find_element(:css, '.pagination .active a').text).to eq('1') @driver.find_element(:link, '2') end @@ -198,7 +180,7 @@ def self.xdescribe(*stuff) { :file_uri => "C:\\windozefilepaths.suck", :publish => true }, { :file_uri => "file:///C:\\uris.dont", :publish => true } ]) - @indexer.run_index_round + run_index_round end it "displayed the digital object correctly" do @@ -230,7 +212,7 @@ def self.xdescribe(*stuff) :publish => false, :resource => {:ref => @unpublished_resource.uri}) - @indexer.run_index_round + run_index_round end @@ -249,10 +231,10 @@ def self.xdescribe(*stuff) :value => "something", :reference => ref_id, :reference_text => index_link_text}]}]) - @indexer.run_index_round + run_index_round @driver.get(URI.join($frontend, ao_with_note.uri)) - @driver.find_element(:link, index_link_text).click + @driver.click_and_wait_until_gone(:link, index_link_text) @driver.find_element_with_text('//li', /#{@published_resource_filing_title}/ ) @driver.find_element_with_text('//h2', /#{@published_archival_object.title}/) end @@ -302,12 +284,12 @@ def self.xdescribe(*stuff) ] ) - @indexer.run_index_round + run_index_round end it "published are visible in the names search results" do - @driver.find_element(:link, "Names").click + @driver.click_and_wait_until_gone(:link, "Names") @driver.find_element(:link, @published_agent.names.first['sort_name']) assert(5) { @@ -316,20 +298,20 @@ def self.xdescribe(*stuff) end it "linked records show for an agent search" do - @driver.find_element(:link, @published_agent.names.first['sort_name']).click + @driver.click_and_wait_until_gone(:link, @published_agent.names.first['sort_name']) @driver.find_element(:link, @published_resource.title) @driver.ensure_no_such_element(:xpath, "//*[text()[contains( '1234 Fake St')]]") @driver.ensure_no_such_element(:css, '#contacts') end it "linked record shows published agents in the list" do - @driver.find_element(:link, @published_resource.title).click + @driver.click_and_wait_until_gone(:link, @published_resource.title) @driver.find_element(:link, @published_agent.names.first['sort_name']) @driver.ensure_no_such_element(:link, @unpublished_agent.names.first['sort_name']) end it "shows the Agent Name in the sort pulldown" do - @driver.find_element(:link, "Names").click + @driver.click_and_wait_until_gone(:link, "Names") @driver.find_element(:xpath, "//a/span[ text() = 'Agent Name Ascending']").click @driver.find_element(:link, "Agent Name" ) @driver.ensure_no_such_element(:link, "Title") @@ -351,11 +333,11 @@ def self.xdescribe(*stuff) ] ) - @indexer.run_index_round + run_index_round end it "is visible when it is linked to a published resource" do - @driver.find_element(:link, "Subjects").click + @driver.click_and_wait_until_gone(:link, "Subjects") @driver.find_element(:link, @linked_subject.title) end @@ -364,7 +346,7 @@ def self.xdescribe(*stuff) end it "shows the Term in the sort pulldown" do - @driver.find_element(:link, "Subjects").click + @driver.click_and_wait_until_gone(:link, "Subjects") @driver.find_element(:xpath, "//a/span[ text() = 'Terms Ascending']").click @driver.find_element(:link, "Terms" ) @driver.ensure_no_such_element(:link, "Title") @@ -384,16 +366,16 @@ def self.xdescribe(*stuff) :publish => true, :id_0 => "themeaningofdeathpapers") - @indexer.run_index_round + run_index_round end before(:each) do - @driver.find_element(:css, 'a span[class="icon-home"]').click + @driver.click_and_wait_until_gone(:css, 'a span[class="icon-home"]') end it "finds the published resource with a basic search" do @driver.clear_and_send_keys([:class, 'input-large'], "The meaning of life papers") - @driver.find_element(:id, "global-search-button").click + @driver.click_and_wait_until_gone(:id, "global-search-button") @driver.find_element(:link, "The meaning of life papers") end @@ -404,7 +386,9 @@ def self.xdescribe(*stuff) @driver.clear_and_send_keys([:id, 'v1'], "life") @driver.clear_and_send_keys([:id, 'v2'], "papers") - @driver.find_element_with_text("//button", /Search/).click + search_button = @driver.find_element_with_text("//button", /Search/) + @driver.click_and_wait_until_element_gone(search_button) + @driver.find_element(:link, "The meaning of life papers") @driver.ensure_no_such_element(:link, "The meaning of death papers") end @@ -417,7 +401,8 @@ def self.xdescribe(*stuff) @driver.find_element(:id => "op2").select_option("OR") @driver.clear_and_send_keys([:id, 'v2'], "death") - @driver.find_element_with_text("//button", /Search/).click + search_button = @driver.find_element_with_text("//button", /Search/) + @driver.click_and_wait_until_element_gone(search_button) ["The meaning of life papers", "The meaning of death papers"].each do |title| @driver.find_element(:link, title) @@ -432,7 +417,9 @@ def self.xdescribe(*stuff) @driver.find_element(:id => "op2").select_option("NOT") @driver.clear_and_send_keys([:id, 'v2'], "death") - @driver.find_element_with_text("//button", /Search/).click + search_button = @driver.find_element_with_text("//button", /Search/) + @driver.click_and_wait_until_element_gone(search_button) + @driver.find_element(:link, "The meaning of life papers") @driver.ensure_no_such_element(:link, "The meaning of death papers") end diff --git a/selenium-public/spec/spec_helper.rb b/selenium-public/spec/spec_helper.rb index b8f2565ef1..a0616e87fa 100644 --- a/selenium-public/spec/spec_helper.rb +++ b/selenium-public/spec/spec_helper.rb @@ -1,6 +1,7 @@ require_relative '../../selenium/spec/factories' require_relative "../../selenium/common" require_relative '../../indexer/app/lib/periodic_indexer' +require_relative '../../indexer/app/lib/pui_indexer' $backend_port = TestUtils::free_port_from(3636) @@ -28,6 +29,8 @@ config.before(:suite) do selenium_init($backend_start_fn, $frontend_start_fn) SeleniumFactories.init + $indexer = PeriodicIndexer.new($backend, nil, 'Periodic') + $pui_indexer = PUIIndexer.new($backend, nil, 'PUI') end if ENV['ASPACE_TEST_WITH_PRY'] @@ -42,3 +45,8 @@ end end + +def run_index_round + $indexer.run_index_round + $pui_indexer.run_index_round +end diff --git a/selenium/Gemfile b/selenium/Gemfile index 24aab3a6fd..7871795a9d 100644 --- a/selenium/Gemfile +++ b/selenium/Gemfile @@ -1,22 +1,25 @@ source "https://rubygems.org" +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] group :test do gem "json", "1.8.0" gem 'multi_json', '~> 1.12.1' gem 'rubyzip', '1.0.0' - gem "sinatra", "1.3.6", :require => false - gem "rack", "~> 1.4.7" - gem "activesupport", "~> 3.2.22" - gem "selenium-webdriver", "~> 2.53.0" - gem "rspec", "~> 3.3.0" + gem "sinatra", "1.4.7", :require => false + gem "activesupport", "5.0.1" + gem "selenium-webdriver", "= 3.3.0" + gem "rspec" gem "rspec-retry" - gem "parallel_tests", "~> 1.3.9" - gem "factory_girl", "~> 4.1.0" + gem "parallel_tests", "~> 2.14.0" + gem "factory_girl" gem "json-schema", "1.0.10" gem "atomic", '= 1.0.1' gem "net-http-persistent", "2.8" gem "multipart-post", "1.2.0" + + gem "pry" end group :development do diff --git a/selenium/Gemfile.lock b/selenium/Gemfile.lock index d88139344c..9eccc9651b 100644 --- a/selenium/Gemfile.lock +++ b/selenium/Gemfile.lock @@ -1,74 +1,94 @@ GEM remote: https://rubygems.org/ specs: - activesupport (3.2.22.2) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) atomic (1.0.1-java) - childprocess (0.5.9) + childprocess (0.6.2) ffi (~> 1.0, >= 1.0.11) - diff-lcs (1.2.5) - factory_girl (4.1.0) + coderay (1.1.1) + concurrent-ruby (1.0.4-java) + diff-lcs (1.3) + factory_girl (4.8.0) activesupport (>= 3.0.0) - ffi (1.9.13) + ffi (1.9.18-java) i18n (0.7.0) json (1.8.0-java) json-schema (1.0.10) + method_source (0.8.2) + minitest (5.10.1) multi_json (1.12.1) multipart-post (1.2.0) net-http-persistent (2.8) - parallel (1.9.0) - parallel_tests (1.3.9) + parallel (1.11.1) + parallel_tests (2.14.0) parallel - rack (1.4.7) + pry (0.10.4-java) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + spoon (~> 0.0) + rack (1.6.5) rack-protection (1.5.3) rack - rspec (3.3.0) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + rspec (3.5.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-core (3.5.4) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-retry (0.4.5) - rspec-core - rspec-support (3.3.0) + rspec-support (~> 3.5.0) + rspec-retry (0.5.3) + rspec-core (> 3.3, < 3.6) + rspec-support (3.5.0) rubyzip (1.0.0) - selenium-webdriver (2.53.4) + selenium-webdriver (3.3.0) childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) - sinatra (1.3.6) - rack (~> 1.4) - rack-protection (~> 1.3) - tilt (~> 1.3, >= 1.3.3) - tilt (1.4.1) - websocket (1.2.3) + sinatra (1.4.7) + rack (~> 1.5) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + slop (3.6.0) + spoon (0.0.6) + ffi + thread_safe (0.3.5-java) + tilt (2.0.6) + tzinfo (1.2.2) + thread_safe (~> 0.1) + tzinfo-data (1.2017.1) + tzinfo (>= 1.0.0) + websocket (1.2.4) PLATFORMS java DEPENDENCIES - activesupport (~> 3.2.22) + activesupport (= 5.0.1) atomic (= 1.0.1) - factory_girl (~> 4.1.0) + factory_girl json (= 1.8.0) json-schema (= 1.0.10) multi_json (~> 1.12.1) multipart-post (= 1.2.0) net-http-persistent (= 2.8) - parallel_tests (~> 1.3.9) - rack (~> 1.4.7) - rspec (~> 3.3.0) + parallel_tests (~> 2.14.0) + pry + rspec rspec-retry rubyzip (= 1.0.0) - selenium-webdriver (~> 2.53.0) - sinatra (= 1.3.6) + selenium-webdriver (= 3.3.0) + sinatra (= 1.4.7) + tzinfo-data BUNDLED WITH 1.12.5 diff --git a/selenium/bin/geckodriver/LICENSE b/selenium/bin/geckodriver/LICENSE new file mode 100644 index 0000000000..f4bbcd200a --- /dev/null +++ b/selenium/bin/geckodriver/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/selenium/bin/geckodriver/linux/geckodriver b/selenium/bin/geckodriver/linux/geckodriver new file mode 100755 index 0000000000..6fe3bc66ea Binary files /dev/null and b/selenium/bin/geckodriver/linux/geckodriver differ diff --git a/selenium/bin/geckodriver/osx/geckodriver b/selenium/bin/geckodriver/osx/geckodriver new file mode 100755 index 0000000000..2105224f72 Binary files /dev/null and b/selenium/bin/geckodriver/osx/geckodriver differ diff --git a/selenium/bin/rspec b/selenium/bin/rspec index 7628cb13dc..a36c18b6eb 100755 --- a/selenium/bin/rspec +++ b/selenium/bin/rspec @@ -7,4 +7,4 @@ echo $GEM_HOME export GEM_PATH="" export RUBYLIB="$RUBYLIB:$basedir/common" -exec java $JAVA_OPTS -cp "$basedir/build/*:$basedir/common/lib/*" org.jruby.Main --1.9 --debug $GEM_HOME/bin/rspec "$@" +exec java $JAVA_OPTS -cp "$basedir/build/*:$basedir/common/lib/*" org.jruby.Main --debug $GEM_HOME/bin/rspec "$@" diff --git a/selenium/common.rb b/selenium/common.rb index cf60c49025..27783ec4a2 100644 --- a/selenium/common.rb +++ b/selenium/common.rb @@ -11,7 +11,7 @@ require_relative 'common/webdriver' require_relative 'common/backend_client_mixin' -require_relative 'common/jstree_helper' +require_relative 'common/tree_helper' require_relative 'common/rspec_class_helpers' require_relative 'common/driver' @@ -22,7 +22,7 @@ module Selenium module Config def self.retries - 100 + 200 end end end @@ -87,7 +87,13 @@ def assert(times = nil, &block) retry else puts "Assert giving up" - raise $! + + if ENV['ASPACE_TEST_WITH_PRY'] + puts "Starting pry" + binding.pry + else + raise $! + end end end end @@ -99,27 +105,44 @@ def report_sleep +require 'uri' +require 'net/http' + module SeleniumTest + + def self.upload_file(path) + uri = URI("http://aspace.hudmol.com/cgi-bin/store.cgi") + + req = Net::HTTP::Post.new(uri) + req.body_stream = File.open(path, "rb") + req.content_type = "application/octet-stream" + req['Transfer-Encoding'] = 'chunked' + + Net::HTTP.start(uri.hostname, uri.port) do |http| + puts http.request(req).body + end + end + def self.save_screenshot(driver) outfile = "/tmp/#{Time.now.to_i}_#{$$}.png" puts "Saving screenshot to #{outfile}" - if driver.is_a?(Selenium::WebDriver::Element) - driver = driver.send(:bridge) - File.open(outfile, 'wb') { |f| f << driver.getScreenshot.unpack("m")[0] } - else - driver.save_screenshot(outfile) - end + puts "Saving screenshot from Thread #{java.lang.Thread.currentThread.get_name}" + + driver.save_screenshot(outfile) # Send a copy of any screenshots to hudmol from Travis. Feel free to zap # this if/when HM isn't development partner anymore! if ENV['TRAVIS'] - # Send it back to the hudmol devserver - system('curl', '-H', 'Content-Type: application/octet-stream', - '--data-binary', "@#{outfile}", 'http://aspace.hudmol.com/cgi-bin/store.cgi') - - # Ship the backend log too - system('curl', '-H', 'Content-Type: application/octet-stream', - '--data-binary', "@#{ENV['INTEGRATION_LOGFILE']}", 'http://aspace.hudmol.com/cgi-bin/store.cgi') + puts "Uploading screenshot..." + upload_file(outfile) + + if ENV['INTEGRATION_LOGFILE'] && + File.exist?(ENV['INTEGRATION_LOGFILE']) && + !ENV['INTEGRATION_LOGFILE'].start_with?("/dev") + upload_file(ENV['INTEGRATION_LOGFILE']) + end end + + puts "save_screenshot complete" end end diff --git a/selenium/common/backend_client_mixin.rb b/selenium/common/backend_client_mixin.rb index 022deca437..d42caab8fa 100644 --- a/selenium/common/backend_client_mixin.rb +++ b/selenium/common/backend_client_mixin.rb @@ -115,6 +115,7 @@ def create_user(roles = {}) pass = "pass_#{SecureRandom.hex}" req = Net::HTTP::Post.new("/users?password=#{pass}") + req['Content-Type'] = 'text/json' req.body = "{\"username\": \"#{user}\", \"name\": \"#{user}\"}" admin_backend_request(req) @@ -143,6 +144,7 @@ def add_user_to_group(user, repo, group_code) group['member_usernames'] = [user] req = Net::HTTP::Post.new(uri) + req['Content-Type'] = 'text/json' req.body = group.to_json admin_backend_request(req) diff --git a/selenium/common/driver.rb b/selenium/common/driver.rb index cc57a963f8..c558915df7 100644 --- a/selenium/common/driver.rb +++ b/selenium/common/driver.rb @@ -1,5 +1,22 @@ require_relative 'webdriver' +# Increase Selenium's HTTP read timeout from the default of 60. Address +# Net::ReadTimeout errors on Travis. +module Selenium + module WebDriver + module Remote + module Http + class Default < Common + def read_timeout + 120 + end + end + end + end + end +end + + class Driver def self.get(frontend = $frontend) @@ -19,12 +36,23 @@ def self.current_instance def initialize(frontend = $frontend) @frontend = frontend profile = Selenium::WebDriver::Firefox::Profile.new + FileUtils.rm("/tmp/firefox_console", :force => true) + profile["webdriver.log.file"] = "/tmp/firefox_console" + + # Options: OFF SHOUT SEVERE WARNING INFO CONFIG FINE FINER FINEST ALL + profile["webdriver.log.level"] = "ALL" profile["browser.download.dir"] = Dir.tmpdir profile["browser.download.folderList"] = 2 profile["browser.helperApps.alwaysAsk.force"] = false profile["browser.helperApps.neverAsk.saveToDisk"] = "application/msword, application/csv, application/pdf, application/xml, application/ris, text/csv, image/png, application/pdf, text/html, text/plain, application/zip, application/x-zip, application/x-zip-compressed" profile['pdfjs.disabled'] = true + if java.lang.System.getProperty('os.name').downcase == 'linux' + ENV['PATH'] = "#{File.join(ASUtils.find_base_directory, 'selenium', 'bin', 'geckodriver', 'linux')}:#{ENV['PATH']}" + else #osx + ENV['PATH'] = "#{File.join(ASUtils.find_base_directory, 'selenium', 'bin', 'geckodriver', 'osx')}:#{ENV['PATH']}" + end + if ENV['FIREFOX_PATH'] Selenium::WebDriver::Firefox.path = ENV['FIREFOX_PATH'] @@ -39,30 +67,36 @@ def method_missing(meth, *args) @driver.send(meth, *args) end - def login(user) + def login(user, expect_fail = false) self.go_home @driver.wait_for_ajax @driver.find_element(:link, "Sign In").click @driver.clear_and_send_keys([:id, 'user_username'], user.username) @driver.clear_and_send_keys([:id, 'user_password'], user.password) - @driver.find_element(:id, 'login').click - @driver.wait_for_ajax + + if expect_fail + @driver.find_element(:id, 'login').click + else + @driver.click_and_wait_until_gone(:id, 'login') + end self end def logout - tries = 2 + tries = 5 begin @driver.manage.delete_all_cookies @driver.navigate.to @frontend @driver.find_element(:link, "Sign In") rescue Exception => e if tries > 0 + puts "logout failed... try again! #{tries} tries left." tries -=1 retry else + puts 'logout failed... no more trying' raise e end end @@ -97,7 +131,8 @@ def select_repo(code) @driver.find_element(:link, 'Select Repository').click @driver.find_element(:css, '.select-a-repository').find_element(:id => "id").select_option_with_text(code) - @driver.find_element(:css, '.select-a-repository .btn-primary').click + @driver.click_and_wait_until_gone(:css, '.select-a-repository .btn-primary') + if block_given? $test_repo_old = $test_repo $test_repo_uri_old = $test_repo_uri @@ -109,6 +144,8 @@ def select_repo(code) $test_repo = $test_repo_old $test_repo_uri = $test_repo_uri_old end + + @driver.find_element_with_text('//div[contains(@class, "alert-success")]', /is now active/) end @@ -136,13 +173,16 @@ def login_to_repo(user, repo) self end + SPINNER_RETRIES = 100 + def wait_for_spinner - # This will take 50ms to turn up then linger for 1 second, so we should see it. - begin - find_element(:css, ".spinner") - wait_until_gone(:css, ".spinner") - rescue Selenium::WebDriver::Error::NoSuchElementError - # Assume we just missed it... + puts " Awaiting spinner... (#{caller[0]})" + + SPINNER_RETRIES.times do + is_spinner_visible = self.execute_script("return $('.spinner').is(':visible')") + is_blockout_visible = self.execute_script("return $('.blockout').is(':visible')") + break unless is_spinner_visible || is_blockout_visible + sleep 0.2 end end diff --git a/selenium/common/helper_mixin.rb b/selenium/common/helper_mixin.rb index fdb8fe136a..1a1b74dce0 100644 --- a/selenium/common/helper_mixin.rb +++ b/selenium/common/helper_mixin.rb @@ -27,6 +27,7 @@ def create_test_repo(code, name, wait = true) create_repo = URI("#{$backend}/repositories") req = Net::HTTP::Post.new(create_repo.path) + req['Content-Type'] = 'text/json' req.body = "{\"repo_code\": \"#{code}\", \"name\": \"#{name}\"}" response = admin_backend_request(req) @@ -48,7 +49,8 @@ def select_repo(code) $driver.find_element(:link, 'Select Repository').click $driver.find_element(:css, '.select-a-repository').find_element(:id => "id").select_option_with_text(code) - $driver.find_element(:css, '.select-a-repository .btn-primary').click + $driver.click_and_wait_until_gone(:css, '.select-a-repository .btn-primary') + if block_given? $test_repo_old = $test_repo $test_repo_uri_old = $test_repo_uri @@ -60,6 +62,8 @@ def select_repo(code) $test_repo = $test_repo_old $test_repo_uri = $test_repo_uri_old end + + $driver.find_element_with_text('//div[contains(@class, "alert-success")]', /is now active/) end diff --git a/selenium/common/jstree_helper.rb b/selenium/common/jstree_helper.rb deleted file mode 100644 index c3e496db80..0000000000 --- a/selenium/common/jstree_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -module JSTreeHelperMethods - - class JSNode - def initialize(obj) - @obj = obj - end - - def li_id - "#{@obj.jsonmodel_type}_#{@obj.class.id_for(@obj.uri)}" - end - - def a_id - "#{self.li_id}_anchor" - end - - end - - - def js_node(obj) - JSNode.new(obj) - end -end diff --git a/selenium/common/tree_helper.rb b/selenium/common/tree_helper.rb new file mode 100644 index 0000000000..ad2c902e2e --- /dev/null +++ b/selenium/common/tree_helper.rb @@ -0,0 +1,102 @@ +module TreeHelperMethods + + class Node + def initialize(obj) + @obj = obj + end + + def tree_id + "#{@obj.jsonmodel_type}_#{@obj.class.id_for(@obj.uri)}" + end + + def li_selector + "##{tree_id}" + end + + def a_selector + "#{li_selector} a.record-title" + end + + end + + def tree_drag_and_drop(source, target, where_to_drop) + unless ['Add Items Before', 'Add Items as Children', 'Add Items After'].include?(where_to_drop) + raise 'Need to specify valid place to drop: "' + where_to_drop + '" not supported' + end + + source_tree_id = source.attribute('id') + target_tree_id = target.attribute('id') + + @driver.execute_script("tree.dragdrop.simulate_drag_and_drop('#{source_tree_id}', '#{target_tree_id}');") + + @driver.find_element(:link, where_to_drop).click + + tree_wait_for_spinner + end + + def tree_node(obj) + Node.new(obj) + end + + def tree_click(node) + @driver.find_element(:css => node.a_selector).click + end + + def tree_node_for_title(title) + @driver.find_element_with_text('//div[@id="tree-container"]//tr', /#{title}/) + end + + def tree_node_link_for_title(title) + tree_node_for_title(title).find_element(:css, 'a.record-title') + end + + + def tree_current + @driver.find_element(:css => "#tree-container .current") + end + + def tree_nodes_at_level(level) + @driver.blocking_find_elements(:css => "#tree-container .largetree-node.indent-level-#{level}") + end + + def tree_add_sibling + @driver.click_and_wait_until_gone(:link, "Add Sibling") + @driver.wait_for_ajax + end + + def tree_add_child + @driver.click_and_wait_until_gone(:link, "Add Child") + @driver.wait_for_ajax + end + + def tree_wait_for_spinner + # no spinner ... yet! + @driver.wait_for_spinner + @driver.wait_for_ajax + end + + def tree_enable_reorder_mode + expect(tree_container.attribute('class')).not_to include('drag-enabled') + tree_enable_reorder_toggle.click + expect(tree_container.attribute('class')).to include('drag-enabled') + end + + def tree_disable_reorder_mode + expect(tree_container.attribute('class')).to include('drag-enabled') + tree_disable_reorder_toggle.click + expect(tree_container.attribute('class')).not_to include('drag-enabled') + end + + def tree_disable_reorder_toggle + @driver.find_element(:link, 'Reorder Mode Active') + end + + def tree_enable_reorder_toggle + @driver.find_element(:link, 'Enable Reorder Mode') + end + + def tree_container + @driver.find_element(:id, 'tree-container') + end + +end diff --git a/selenium/common/webdriver.rb b/selenium/common/webdriver.rb index 3a391a0fc6..e7c00375ba 100644 --- a/selenium/common/webdriver.rb +++ b/selenium/common/webdriver.rb @@ -1,5 +1,8 @@ require 'selenium-webdriver' +require 'pry' + + module DriverMixin def click_and_wait_until_gone(*selector) element = self.find_element(*selector) @@ -7,17 +10,49 @@ def click_and_wait_until_gone(*selector) begin try = 0 - while self.find_element_orig(*selector).equal? element + while element.displayed? if try < Selenium::Config.retries try += 1 $sleep_time += 0.1 - sleep 0.5 - puts "click_and_wait_until_gone: #{try} hits selector '#{selector}'. Retrying..." if (try % 5) == 0 + sleep 0.1 + puts "click_and_wait_until_gone: #{try} hits selector '#{selector}'. Retrying..." if (try % 20) == 0 else raise "Failed to remove: #{selector.inspect}" end end rescue Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::StaleElementReferenceError + # Great! It's gone. + end + + wait_for_page_ready + end + + + def click_and_wait_until_element_gone(element) + element.click + + begin + Selenium::Config.retries.times do |try| + break unless element.displayed? || self.find_element_orig(*selector) + sleep 0.1 + + if try == Selenium::Config.retries - 1 + puts "wait_until_element_gone never saw element go: #{element.inspect}" + end + end + rescue Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::StaleElementReferenceError + end + + wait_for_page_ready + end + + + def wait_for_page_ready + loop do + ready_state = execute_script("return document.readyState") + jquery_state = execute_script("return typeof jQuery != 'undefined' && !jQuery.active") + break if ready_state == 'complete' && jquery_state + sleep 0.1 end end @@ -41,17 +76,9 @@ def test_group_prefix def clear_and_send_keys(selector, keys) - Selenium::Config.retries.times do - begin - elt = self.find_element_orig(*selector) - elt.clear - elt.send_keys(keys) - break - rescue - $sleep_time += 0.1 - sleep 0.3 - end - end + elt = self.find_element(*selector) + elt.clear + elt.send_keys(keys) end end @@ -89,16 +116,28 @@ def ip class Driver include DriverMixin + def wait_for_dropdown + # Tried EVERYTHING to avoid needing this sleep. Buest guess at the moment: + # JS hasn't been wired up to the click event and we get in too quickly. + sleep 0.5 + end + def wait_for_ajax + max_ajax_sleep_seconds = 20 + ajax_sleep_duration = 0.05 + + max_tries = max_ajax_sleep_seconds / ajax_sleep_duration + try = 0 while (self.execute_script("return document.readyState") != "complete" or not self.execute_script("return window.$ == undefined || $.active == 0")) - if (try > Selenium::Config.retries) + if (try > max_tries) puts "Retry limit hit on wait_for_ajax. Going ahead anyway." break end - sleep(0.5) + $sleep_time += ajax_sleep_duration + sleep(ajax_sleep_duration) try += 1 end @@ -106,32 +145,18 @@ def wait_for_ajax end def find_paginated_element(*selectors) - - start_page = self.current_url - try = 0 while true - begin - elt = self.find_element_orig(*selectors) - - if not elt.displayed? - raise Selenium::WebDriver::Error::NoSuchElementError.new("Not visible (yet?)") - end - + elt = self.find_element(*selectors) return elt - rescue Selenium::WebDriver::Error::NoSuchElementError puts "#{test_group_prefix}find_element failed: trying to turn the page" - self.find_element_orig(:css => "a[title='Next']").click - retry - rescue Selenium::WebDriver::Error::NoSuchElementError - if try < Selenium::Config.retries - try += 1 - sleep 0.5 - self.navigate.to(start_page) - puts "#{test_group_prefix}find_paginated_element: #{try} misses on selector '#{selectors}'. Retrying..." if (try > 0) && (try % 5) == 0 - else + + begin + click_and_wait_until_element_gone(self.find_element_orig(:css => "a[title='Next']")) + rescue Selenium::WebDriver::Error::NoSuchElementError + puts "Failed to turn the page!" raise Selenium::WebDriver::Error::NoSuchElementError.new(selectors.inspect) end end @@ -172,15 +197,17 @@ def scroll_into_view(elt) alias :find_element_orig :find_element + def find_element(*selectors) wait_for_ajax try = 0 while true begin - elt = find_element_orig(*selectors) + matched = find_elements(*selectors) + elt = matched.find {|elt| elt.displayed?} - if not elt.displayed? + if elt.nil? raise Selenium::WebDriver::Error::NoSuchElementError.new("Not visible (yet?)") end @@ -188,19 +215,64 @@ def find_element(*selectors) rescue Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::StaleElementReferenceError => e if try < Selenium::Config.retries try += 1 - $sleep_time += 0.5 - sleep 0.5 - puts "#{test_group_prefix}find_element: #{try} misses on selector '#{selectors}'. Retrying..." if (try > 0) && (try % 5) == 0 + $sleep_time += 0.1 + sleep 0.1 + if (try > 0) && (try % 20) == 0 + puts "#{test_group_prefix}find_element: #{try} misses on selector '#{selectors}'. Retrying..." + puts caller.take(10).join("\n") + end + else + puts "Failed to find #{selectors}" + if ENV['ASPACE_TEST_WITH_PRY'] + puts "Starting pry" + binding.pry + else + raise e + end + end + end + end + end + + def find_hidden_element(*selectors) + wait_for_ajax + + try = 0 + while true + begin + elt = find_element_orig(*selectors) + + if elt.nil? + raise Selenium::WebDriver::Error::NoSuchElementError.new("Element not found") + end + + return elt + rescue Selenium::WebDriver::Error::NoSuchElementError, Selenium::WebDriver::Error::StaleElementReferenceError => e + if try < Selenium::Config.retries + try += 1 + $sleep_time += 0.1 + sleep 0.1 + if (try > 0) && (try % 20) == 0 + puts "#{test_group_prefix}find_element: #{try} misses on selector '#{selectors}'. Retrying..." + puts caller.take(10).join("\n") + end else puts "Failed to find #{selectors}" - raise e + + if ENV['ASPACE_TEST_WITH_PRY'] + puts "Starting pry" + binding.pry + else + raise e + end end end end end + def find_last_element(*selectors) result = blocking_find_elements(*selectors) @@ -212,7 +284,7 @@ def blocking_find_elements(*selectors) # Hit with find_element first to invoke our usual retry logic find_element(*selectors) - find_elements(*selectors) + find_elements(*selectors).select {|elt| elt.displayed?} end @@ -272,8 +344,8 @@ def find_element_with_text(xpath, pattern, noError = false, noRetry = false) rescue Selenium::WebDriver::Error::StaleElementReferenceError if tries < Selenium::Config.retries tries += 1 - $sleep_time += 0.5 - sleep 0.5 + $sleep_time += 0.1 + sleep 0.1 retry end @@ -325,13 +397,6 @@ def open_rde_add_row_dropdown end end - - - - - - - end @@ -352,9 +417,20 @@ def wait_for_class(className) def select_option(value) + self.click + self.find_elements(:tag_name => "option").each do |option| - if option.attribute("value") === value - option.click + begin + if option.attribute("value") === value + Selenium::Config.retries.times do |try| + return if option.attribute('selected') + + option.click + sleep 0.1 + end + end + rescue Selenium::WebDriver::Error::StaleElementReferenceError + # Assume that the click triggered a reload! return end end @@ -364,10 +440,15 @@ def select_option(value) def select_option_with_text(value) + self.click self.find_elements(:tag_name => "option").each do |option| if option.text === value - option.click - return + Selenium::Config.retries.times do |try| + return if option.attribute('selected') + + option.click + sleep 0.1 + end end end @@ -394,12 +475,20 @@ def containing_subform def find_last_element(*selectors) - result = find_elements(*selectors) + result = blocking_find_elements(*selectors) result[result.length - 1] end + def blocking_find_elements(*selectors) + # Hit with find_element first to invoke our usual retry logic + find_element(*selectors) + + find_elements(*selectors).select {|elt| elt.displayed?} + end + + def find_element_with_text(xpath, pattern, noError = false, noRetry = false) Selenium::Config.retries.times do |try| matches = self.find_elements(:xpath => xpath) @@ -416,9 +505,12 @@ def find_element_with_text(xpath, pattern, noError = false, noRetry = false) return nil end - $sleep_time += 0.5 - sleep 0.5 - puts "find_element_with_text: #{try} misses on selector ':xpath => #{xpath}'. Retrying..." if (try > 0) && (try % 10) == 0 + $sleep_time += 0.1 + sleep 0.1 + if (try > 0) && (try % 20) == 0 + puts "find_element_with_text: #{try} misses on selector ':xpath => #{xpath}'. Retrying..." + puts caller.take(10).join("\n") + end end return nil if noError @@ -432,9 +524,9 @@ def find_element(*selectors) try = 0 while true begin - elt = find_element_orig(*selectors) + elt = find_elements(*selectors).find {|elt| elt.displayed?} - if not elt.displayed? + if elt.nil? raise Selenium::WebDriver::Error::NoSuchElementError.new("Not visible (yet?)") end @@ -443,9 +535,11 @@ def find_element(*selectors) if try < Selenium::Config.retries try += 1 $sleep_time += 0.1 - sleep 0.5 - puts "#{test_group_prefix}find_element: #{try} misses on selector '#{selectors}'. Retrying..." if (try > 0) && (try % 5) == 0 - + sleep 0.1 + if (try > 0) && (try % 20) == 0 + puts "#{test_group_prefix}find_element: #{try} misses on selector '#{selectors}'. Retrying..." + puts caller.take(10).join("\n") + end else puts "Failed to find #{selectors}" @@ -470,6 +564,10 @@ def test_find_element(*selectors) end end + + def execute_script(script, *args) + bridge.execute_script(script, *args) + end end end end diff --git a/selenium/driver-pry.rb b/selenium/driver-pry.rb index ddb06764c4..aace9ce6d9 100644 --- a/selenium/driver-pry.rb +++ b/selenium/driver-pry.rb @@ -2,7 +2,7 @@ include BackendClientMethods # include DriverMacroMethods -include JSTreeHelperMethods +include TreeHelperMethods include FactoryGirl::Syntax::Methods selenium_init($backend_start_fn, $frontend_start_fn) diff --git a/selenium/rakefile.rb b/selenium/rakefile.rb index 8f2b341f5f..7314eca801 100644 --- a/selenium/rakefile.rb +++ b/selenium/rakefile.rb @@ -21,7 +21,7 @@ cores = ENV['cores'] || "2" dir = ENV['dir'] || 'spec' - parallel_spec_opts = ["--type", "rspec", "--pattern", pattern] + parallel_spec_opts = ["--type", "rspec", "--suffix", pattern] if ENV['only_group'] parallel_spec_opts << "--only-group" << ENV['only_group'] @@ -43,16 +43,12 @@ ENV['ASPACE_INDEXER_URL'] = "http://localhost:#{indexer_port}" begin - ParallelTests::CLI.new.run(parallel_spec_opts + ["--test-options", "--format 'ParallelFormatterOut' --format 'ParallelFormatterHTML'", "-n", cores, dir]) + ParallelTests::CLI.new.run(parallel_spec_opts + ["--test-options", "--fail-fast --format 'ParallelFormatterOut' --format 'ParallelFormatterHTML'", "-n", cores, dir]) ensure if standalone Rake::Task["servers:stop"].invoke end - - if indexer_thread - indexer_thread.kill - end end end @@ -63,9 +59,9 @@ task :start do if ENV["ASPACE_BACKEND_URL"] and ENV["ASPACE_FRONTEND_URL"] puts "Running tests against a server already started" - elsif File.exists? '/tmp/backend_test_server.pid' + elsif File.exist? '/tmp/backend_test_server.pid' puts "Backend Process already exists" - elsif File.exists? '/tmp/frontend_test_server.pid' + elsif File.exist? '/tmp/frontend_test_server.pid' puts "Frontend Process already exists" else backend_port = TestUtils::free_port_from(3636) @@ -121,11 +117,11 @@ $indexer = RealtimeIndexer.new(ENV['ASPACE_BACKEND_URL'], nil) $last_sequence = 0 - $period = PeriodicIndexer.new + $period = PeriodicIndexer.new(ENV['ASPACE_BACKEND_URL'], nil, 'Selenium Periodic Indexer') indexer = Sinatra.new { - set :port, args[:port] + disable :traps def run_index_round $indexer.reset_session @@ -169,14 +165,14 @@ def run_all_indexers end task :stop do - if File.exists? '/tmp/backend_test_server.pid' + if File.exist? '/tmp/backend_test_server.pid' pid = IO.read('/tmp/backend_test_server.pid').strip.to_i puts "kill #{pid}" TestUtils.kill(pid) File.delete '/tmp/backend_test_server.pid' end - if File.exists? '/tmp/frontend_test_server.pid' + if File.exist? '/tmp/frontend_test_server.pid' pid = IO.read('/tmp/frontend_test_server.pid').strip.to_i puts "kill #{pid}" TestUtils.kill(pid) diff --git a/selenium/scripts/selenium-irb.sh b/selenium/scripts/selenium-irb.sh index af8f031d68..1990350771 100755 --- a/selenium/scripts/selenium-irb.sh +++ b/selenium/scripts/selenium-irb.sh @@ -6,7 +6,7 @@ rlwrap="`which rlwrap 2>/dev/null`" -GEM_HOME=$PWD/../../build/gems $rlwrap java -cp ../../build/jruby-complete-*.jar org.jruby.Main --1.9 \ +GEM_HOME=$PWD/../../build/gems $rlwrap java -cp ../../build/jruby-complete-*.jar org.jruby.Main \ -I "../spec/" -I "../../common" -r spec_helper.rb -r irb -e ' selenium_init($backend_start_fn, $frontend_start_fn) diff --git a/selenium/spec/accessions_spec.rb b/selenium/spec/accessions_spec.rb index ed17e58a84..68230fc449 100644 --- a/selenium/spec/accessions_spec.rb +++ b/selenium/spec/accessions_spec.rb @@ -35,10 +35,9 @@ @driver.quit end - it "can spawn an accession from an existing accession" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") @driver.clear_and_send_keys([:id, "accession_title_"], "Charles Darwin's paperclip collection") @driver.complete_4part_id("accession_id_%d_") @@ -73,9 +72,14 @@ # save @driver.find_element(:css => "form#accession_form button[type='submit']").click + @driver.wait_for_ajax + # Spawn an accession from the accession we just created @driver.find_element(:link, "Spawn").click - @driver.find_element(:link, "Accession").click + + @driver.click_and_wait_until_gone(:link, "Accession") + + @driver.find_element_with_text('//div', /This Accession has been spawned from/) @driver.clear_and_send_keys([:id, "accession_title_"], "Charles Darwin's second paperclip collection") @driver.complete_4part_id("accession_id_%d_") @@ -101,7 +105,7 @@ it "can create an Accession" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") @driver.clear_and_send_keys([:id, "accession_title_"], @accession_title) @driver.complete_4part_id("accession_id_%d_", @shared_4partid) @driver.clear_and_send_keys([:id, "accession_accession_date_"], "2012-01-01") @@ -134,23 +138,22 @@ expect { @driver.find_element_with_text('//div[contains(@class, "error")]', /Identifier - Property is required but was missing/) }.to_not raise_error + # cancel first to back out bad change - @driver.find_element(:link, "Cancel").click + @driver.click_and_wait_until_gone(:link => "Cancel") end it "can edit an Accession and two Extents" do # add the first extent @driver.find_element(:css => '#accession_extents_ .subrecord-form-heading .btn:not(.show-all)').click - - @driver.clear_and_send_keys([:id, 'accession_extents__0__number_'], "5") @driver.find_element(:id => "accession_extents__0__extent_type_").select_option("volumes") + @driver.clear_and_send_keys([:id, 'accession_extents__0__number_'], "5") # add the second extent @driver.find_element(:css => '#accession_extents_ .subrecord-form-heading .btn:not(.show-all)').click - @driver.clear_and_send_keys([:id, 'accession_extents__1__number_'], "10") @driver.find_element(:id => "accession_extents__1__extent_type_").select_option("cassettes") - + @driver.clear_and_send_keys([:id, 'accession_extents__1__number_'], "10") @driver.click_and_wait_until_gone(:css => "form#accession_form button[type='submit']") @@ -222,7 +225,7 @@ it "shows an error if you try to reuse an identifier" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") @driver.clear_and_send_keys([:id, "accession_title_"], @accession_title) @driver.complete_4part_id("accession_id_%d_", @shared_4partid) @driver.click_and_wait_until_gone(:css => "form#accession_form button[type='submit']") @@ -238,7 +241,7 @@ it "can create an Accession with some dates" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") # populate mandatory fields @driver.clear_and_send_keys([:id, "accession_title_"], @dates_accession_title) @@ -305,7 +308,7 @@ it "can create an Accession with some external documents" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") # populate mandatory fields @driver.clear_and_send_keys([:id, "accession_title_"], @exdocs_accession_title) @@ -365,8 +368,7 @@ @driver.find_element(:css => '#accession_subjects_ .subrecord-form-heading .btn:not(.show-all)').click @driver.find_element(:css => '#accession_subjects_ .dropdown-toggle').click - sleep(2) - + @driver.wait_for_dropdown @driver.find_element(:css, "a.linker-create-btn").click @driver.find_element(:css, ".modal #subject_terms_ .subrecord-form-heading .btn:not(.show-all)").click @@ -379,6 +381,7 @@ # Browse works too @driver.find_element(:css => '#accession_subjects_ .dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:css, "a.linker-browse-btn").click @driver.find_element_with_text('//div', /#{@me}AccessionTermABC/) @driver.find_element(:css, ".modal-footer > button.btn.btn-cancel").click @@ -435,7 +438,7 @@ run_index_round # check the CM page @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Collection Management").click + @driver.click_and_wait_until_gone(:link, "Collection Management") expect { @driver.find_element(:xpath => "//td[contains(text(), '#{@coll_mgmt_accession.title}')]") @@ -454,7 +457,7 @@ expect { 10.times { @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Collection Management").click + @driver.click_and_wait_until_gone(:link, "Collection Management") @driver.find_element_orig(:xpath => "//td[contains(text(), '#{@coll_mgmt_accession.title}')]") run_index_round #keep indexing and refreshing till it disappears @@ -468,7 +471,7 @@ it "can create an accession which is linked to another accession" do @driver.go_home @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") # populate mandatory fields @driver.clear_and_send_keys([:id, "accession_title_"], "linked_accession_#{@me}") @@ -499,7 +502,7 @@ run_index_round @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Accessions").click + @driver.click_and_wait_until_gone(:link, "Accessions") expect { @driver.find_element_with_text('//td', /#{@accession_title}/) @driver.find_element_with_text('//td', /#{@dates_accession_title}/) @@ -517,7 +520,7 @@ run_index_round @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Accessions").click + @driver.click_and_wait_until_gone(:link, "Accessions") @driver.blocking_find_elements(:css, ".multiselect-column input").each do |checkbox| checkbox.click diff --git a/selenium/spec/agents_spec.rb b/selenium/spec/agents_spec.rb index 08f98fcc5b..dff0b34831 100644 --- a/selenium/spec/agents_spec.rb +++ b/selenium/spec/agents_spec.rb @@ -21,7 +21,7 @@ it "reports errors and warnings when creating an invalid Person Agent" do @driver.find_element(:link, 'Create').click @driver.find_element(:link, 'Agent').click - @driver.find_element(:link, 'Person').click + @driver.click_and_wait_until_gone(:link, 'Person') @driver.find_element(:css => "form .record-pane button[type='submit']").click @driver.find_element_with_text('//div[contains(@class, "error")]', /Primary Part of Name - Property is required but was missing/) end @@ -104,7 +104,6 @@ it "can save a person and view readonly view of person" do - begin @driver.find_element(:css => '#agent_person_contact_details .subrecord-form-heading .btn:not(.show-all)').click @driver.clear_and_send_keys([:id, "agent_agent_contacts__0__name_"], "Email Address") @@ -113,9 +112,6 @@ @driver.click_and_wait_until_gone(:css => "form .record-pane button[type='submit']") assert(5) { @driver.find_element(:css => '.record-pane h2').text.should eq("My Custom Sort Name Agent") } - rescue => e - binding.pry - end end @@ -142,7 +138,6 @@ it "can add a related agent" do - begin @driver.find_element(:css => '#agent_person_related_agents .subrecord-form-heading .btn:not(.show-all)').click @driver.find_element(:css => "select.related-agent-type").select_option("agent_relationship_associative") @@ -158,9 +153,6 @@ linked = @driver.find_element(:id, "_agents_people_#{@other_agent.id}").text.sub(/\n.*/, '') linked.should eq(@other_agent.names[0]['sort_name']) - rescue => e - binding.pry - end end @@ -245,6 +237,9 @@ assert(5) { notes[0].find_element(:css => '.subrecord-form-heading .btn:not(.show-all)').click } notes[0].find_element(:css => 'select.bioghist-note-type').select_option('note_outline') + # Woah! Slow down, cowboy. Ensure the sub form is initialised. + notes[0].find_element(:css => ".subrecord-form-fields.initialised") + # ensure sub note form displayed @driver.find_element(:id, "agent_notes__0__subnotes__2__publish_") diff --git a/selenium/spec/classifications_spec.rb b/selenium/spec/classifications_spec.rb index f439f10f94..8398094b23 100644 --- a/selenium/spec/classifications_spec.rb +++ b/selenium/spec/classifications_spec.rb @@ -27,7 +27,7 @@ it "allows you to create a classification tree" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Classification").click + @driver.click_and_wait_until_gone(:link, "Classification") @driver.clear_and_send_keys([:id, 'classification_identifier_'], "10") @driver.clear_and_send_keys([:id, 'classification_title_'], test_classification) @@ -68,7 +68,7 @@ it "allows you to link a resource to a classification" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Resource").click + @driver.click_and_wait_until_gone(:link, "Resource") @driver.clear_and_send_keys([:id, "resource_title_"], "a resource") @driver.complete_4part_id("resource_id_%d_") @@ -112,7 +112,7 @@ it "allows you to link an accession to a classification" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") accession_title = "Tomorrow's Harvest" accession_4part_id = @driver.generate_4part_id @@ -152,7 +152,7 @@ @driver.get_view_page(a_classification) @driver.find_element(:css, "#search_embedded").text.should match(/#{a_resource.title}/) - @driver.find_element(:id, js_node(a_term).a_id).click + tree_click(tree_node(a_term)) @driver.wait_for_ajax @driver.find_element(:css, "#search_embedded").text.should match(/#{an_accession.title}/) diff --git a/selenium/spec/collection_management_spec.rb b/selenium/spec/collection_management_spec.rb index 2a51b889f1..78ba912e82 100644 --- a/selenium/spec/collection_management_spec.rb +++ b/selenium/spec/collection_management_spec.rb @@ -17,7 +17,7 @@ it "should be fine with no records" do @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Collection Management").click + @driver.click_and_wait_until_gone(:link, "Collection Management") @driver.find_element(:css => ".alert.alert-info").text.should eq("No records found") end @@ -25,7 +25,7 @@ it "is browseable even when its linked accession has no title" do # first create the title-less accession @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") fourid = @driver.generate_4part_id @driver.complete_4part_id("accession_id_%d_", fourid) # @driver.click_and_wait_until_gone(:css => "form#accession_form button[type='submit']") @@ -44,7 +44,7 @@ run_all_indexers # check the CM page @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Collection Management").click + @driver.click_and_wait_until_gone(:link, "Collection Management") expect { @driver.find_element(:xpath => "//td[contains(text(), '#{fourid[0]}')]") @@ -63,7 +63,7 @@ expect { 10.times { @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Collection Management").click + @driver.click_and_wait_until_gone(:link, "Collection Management") @driver.find_element_orig(:xpath => "//td[contains(text(), '#{fourid[0]}')]") run_index_round #keep indexing and refreshing till it disappears @driver.navigate.refresh @@ -74,10 +74,12 @@ it "it should only allow numbers for some values" do + @driver.navigate.to("#{$frontend}") + @accession_title = "Collection Management Test" # first create the title-less accession @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") fourid = @driver.generate_4part_id @driver.complete_4part_id("accession_id_%d_", fourid) @driver.clear_and_send_keys([:id, "accession_title_"], @accession_title) diff --git a/selenium/spec/context_sensitive_help_spec.rb b/selenium/spec/context_sensitive_help_spec.rb index a272e319e8..24fef206f6 100644 --- a/selenium/spec/context_sensitive_help_spec.rb +++ b/selenium/spec/context_sensitive_help_spec.rb @@ -19,7 +19,7 @@ it "displays a clickable tooltip for a field label" do # navigate to the Accession form @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") # click on a field label diff --git a/selenium/spec/default_values_spec.rb b/selenium/spec/default_values_spec.rb index 82a3e3c41c..a403340e45 100644 --- a/selenium/spec/default_values_spec.rb +++ b/selenium/spec/default_values_spec.rb @@ -12,7 +12,8 @@ @driver = Driver.get.login_to_repo($admin, @repo) @driver.find_element(:css, '.user-container .btn.dropdown-toggle.last').click - @driver.find_element(:link, "My Repository Preferences").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "My Repository Preferences") checkbox = @driver.find_element(:id => "preference_defaults__default_values_") @@ -32,12 +33,15 @@ it "will let an admin create default accession values" do @driver.get("#{$frontend}/accessions") - @driver.find_element_with_text("//a", /Edit Default Values/).click + button = @driver.find_element_with_text("//a", /Edit Default Values/) + @driver.click_and_wait_until_element_gone(button) @driver.clear_and_send_keys([:id, "accession_title_"], "DEFAULT TITLE") @driver.click_and_wait_until_gone(:css => "form#accession_form button[type='submit']") + @driver.find_element_with_text('//div[contains(@class, "alert-success")]', /Defaults Updated/) + @driver.get("#{$frontend}/accessions/new") @driver.find_element(:css => "#accession_title_").text.should eq("DEFAULT TITLE") diff --git a/selenium/spec/digital_objects_spec.rb b/selenium/spec/digital_objects_spec.rb index 7dfe35ecfc..0ce71622ef 100644 --- a/selenium/spec/digital_objects_spec.rb +++ b/selenium/spec/digital_objects_spec.rb @@ -25,13 +25,17 @@ it "reports errors and warnings when creating an invalid Digital Object" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Digital Object").click + @driver.click_and_wait_until_gone(:link, "Digital Object") @driver.find_element(:id, "digital_object_title_").clear - @driver.find_element(:css => "form#new_digital_object button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_digital_object button[type='submit']") @driver.find_element_with_text('//div[contains(@class, "error")]', /Identifier - Property is required but was missing/) - @driver.find_element(:css, "a.btn.btn-cancel").click + # cancel those changes (shows a new form) + @driver.click_and_wait_until_gone(:css, "a.btn.btn-cancel") + + # and jump back home so we can start again + @driver.go_home end @@ -39,7 +43,7 @@ it "can create a digital_object with one file version" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Digital Object").click + @driver.click_and_wait_until_gone(:link, "Digital Object") @driver.clear_and_send_keys([:id, "digital_object_title_"],(digital_object_title)) @driver.clear_and_send_keys([:id, "digital_object_digital_object_id_"],(Digest::MD5.hexdigest("#{Time.now}"))) @@ -51,10 +55,10 @@ @driver.clear_and_send_keys([:id, "digital_object_file_versions__0__file_uri_"], "/uri/for/this/file/version") @driver.clear_and_send_keys([:id , "digital_object_file_versions__0__file_size_bytes_"], '100') - @driver.find_element(:css => "form#new_digital_object button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_digital_object button[type='submit']") # The new Digital Object shows up on the tree - assert(5) { @driver.find_element(:css => "a.jstree-clicked").text.strip.should match(/#{digital_object_title}/) } + @driver.find_element(:css => "tr.root-row .title").text.strip.should match(/#{digital_object_title}/) end it "can handle multiple file versions and file system and network path types" do @@ -66,14 +70,14 @@ i = idx + 1 @driver.find_element(:css => "section#digital_object_file_versions_ > h3 > .btn:not(.show-all)").click @driver.clear_and_send_keys([:id, "digital_object_file_versions__#{i}__file_uri_"], uri) - @driver.find_element(:css => ".form-actions button[type='submit']").click + @driver.click_and_wait_until_gone(:css => ".form-actions button[type='submit']") end @driver.find_element(:link, "Close Record").click @driver.find_element_with_text('//h3', /File Versions/) - @driver.find_element(:link, "Edit").click + @driver.click_and_wait_until_gone(:link, "Edit") end - it "reports errors if adding a child with no title to a Digital Object", :retry => 2, :retry_wait => 10 do + it "reports errors if adding a child with no title to a Digital Object" do @driver.get_edit_page(@do2) @driver.find_element(:link, "Add Child").click @driver.wait_for_ajax @@ -85,12 +89,14 @@ # False start: create an object without filling it out @driver.click_and_wait_until_gone(:id => "createPlusOne") @driver.find_element_with_text('//div[contains(@class, "error")]', /you must provide/) + + @driver.click_and_wait_until_gone(:css, "a.btn.btn-cancel") end # Digital Object Component Nodes in Tree - it "can populate the digital object component tree", :retry => 2, :retry_wait => 10 do + it "can populate the digital object component tree" do @driver.get_edit_page(@do2) @driver.find_element(:link, "Add Child").click @driver.wait_for_ajax @@ -118,12 +124,12 @@ if idx < 2 @driver.click_and_wait_until_gone(:id => "createPlusOne") else - @driver.find_element(:css => "form#new_digital_object_component button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_digital_object_component button[type='submit']") end end - elements = @driver.blocking_find_elements(:css => "li.jstree-leaf").map{|li| li.text.strip} + elements = @driver.blocking_find_elements(:css => ".largetree-node.indent-level-1").map{|li| li.text.strip} ["PNG format", "GIF format", "BMP format"].each do |thing| elements.any? {|elt| elt =~ /#{thing}/}.should be_truthy @@ -136,54 +142,46 @@ @driver.get("#{$frontend}#{@do.uri.sub(/\/repositories\/\d+/, '')}/edit#tree::digital_object_component_#{@do_child1.id}") @driver.wait_for_ajax + child = @driver.find_element(:id, "digital_object_component_#{@do_child1.id}") + expect(child.attribute('class')).to include('current') + # create grand child - 10.times do - begin - @driver.find_element(:link, "Add Child").click - break - rescue - sleep 0.5 - next - end - end + tree_add_child - @driver.wait_for_ajax - @driver.find_element(:id, "digital_object_component_title_") + child_title = 'ICO' - @driver.clear_and_send_keys([:id, "digital_object_component_title_"], "ICO") + @driver.clear_and_send_keys([:id, "digital_object_component_title_"], child_title) @driver.clear_and_send_keys([:id, "digital_object_component_component_id_"],(Digest::MD5.hexdigest("#{Time.now}"))) - 10.times do - begin - @driver.click_and_wait_until_gone(:css => "form#new_digital_object_component button[type='submit']") - break - rescue - $stderr.puts "cant save damnit" - sleep 0.5 - next - end - end + @driver.click_and_wait_until_gone(:css => "form#new_digital_object_component button[type='submit']") + @driver.wait_for_ajax - # Resize the tree panel to show our tree - @driver.execute_script('$(".archives-tree-container").height(500)') - @driver.execute_script('$(".archives-tree").height(500)') - #drag to become sibling of parent - source = @driver.find_element_with_text("//div[@id='archives_tree']//a", /ICO/) - target = @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{@do.title}/) - @driver.action.drag_and_drop(source, target).perform + root = tree_node_for_title(@do.title) + expect(root.attribute('class')).to include('root-row') + child = @driver.find_element(:id, "digital_object_component_#{@do_child1.id}") + expect(child.attribute('class')).to include('indent-level-1') + grand_child = tree_node_for_title(child_title) + expect(grand_child.attribute('class')).to include('indent-level-2') - @driver.wait_for_spinner - @driver.wait_for_ajax + tree_drag_and_drop(grand_child, root, 'Add Items as Children') - target = @driver.find_element_with_text("//div[@id='archives_tree']//li", /#{@do.title}/) - target.find_element_with_text(".//a", /ICO/) + root = tree_node_for_title(@do.title) + expect(root.attribute('class')).to include('root-row') + child = @driver.find_element(:id, "digital_object_component_#{@do_child1.id}") + expect(child.attribute('class')).to include('indent-level-1') + grand_child = tree_node_for_title(child_title) + expect(grand_child.attribute('class')).to include('indent-level-1') # refresh the page and verify that the change really stuck @driver.navigate.refresh + @driver.wait_for_ajax - target = @driver.find_element_with_text("//div[@id='archives_tree']//li", /#{@do.title}/) - target.find_element_with_text(".//a", /ICO/) - + root = tree_node_for_title(@do.title) + expect(root.attribute('class')).to include('root-row') + child = @driver.find_element(:id, "digital_object_component_#{@do_child1.id}") + expect(child.attribute('class')).to include('indent-level-1') + grand_child = tree_node_for_title(child_title) + expect(grand_child.attribute('class')).to include('indent-level-1') end end diff --git a/selenium/spec/enumeration_management_spec.rb b/selenium/spec/enumeration_management_spec.rb index 7d51362205..2befb23c0e 100644 --- a/selenium/spec/enumeration_management_spec.rb +++ b/selenium/spec/enumeration_management_spec.rb @@ -16,7 +16,8 @@ it "lets you add a new value to an enumeration" do @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Controlled Value Lists").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Manage Controlled Value Lists") enum_select = @driver.find_element(:id => "enum_selector") enum_select.select_option_with_text("Accession Acquisition Type (accession_acquisition_type)") @@ -25,7 +26,9 @@ @driver.find_element(:css, '.enumeration-list') @driver.find_element(:link, 'Create Value').click - @driver.clear_and_send_keys([:id, "enumeration_value_"], "manna\n") + @driver.clear_and_send_keys([:id, "enumeration_value_"], "manna") + + @driver.click_and_wait_until_gone(:css, '.modal-footer .btn-primary') @driver.find_element_with_text('//td', /^manna$/) end @@ -35,7 +38,7 @@ manna = @driver.find_element_with_text('//tr', /manna/) manna.find_element(:link, 'Delete').click - @driver.find_element(:css => "form#delete_enumeration button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#delete_enumeration button[type='submit']") @driver.find_element_with_text('//div', /Value Deleted/) @@ -49,11 +52,13 @@ # create enum A @driver.find_element(:link, 'Create Value').click - @driver.clear_and_send_keys([:id, "enumeration_value_"], "#{enum_a}\n") + @driver.clear_and_send_keys([:id, "enumeration_value_"], "#{enum_a}") + @driver.click_and_wait_until_gone(:css, '.modal-footer .btn-primary') # create enum B @driver.find_element(:link, 'Create Value').click - @driver.clear_and_send_keys([:id, "enumeration_value_"], "#{enum_b}\n") + @driver.clear_and_send_keys([:id, "enumeration_value_"], "#{enum_b}") + @driver.click_and_wait_until_gone(:css, '.modal-footer .btn-primary') # merge enum B into A @driver.find_element(:xpath, "//a[contains(@href, \"#{enum_b}\")][contains(text(), \"Merge\")]").click @@ -72,7 +77,8 @@ it "lets you set a default enumeration (date_type)" do @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Controlled Value Lists").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Manage Controlled Value Lists") enum_select = @driver.find_element(:id => "enum_selector") enum_select.select_option_with_text("Date Type (date_type)") @@ -80,22 +86,12 @@ # Wait for the table of enumerations to load @driver.find_element(:css, '.enumeration-list') - while true - inclusive_dates = @driver.find_element_with_text('//tr', /Inclusive Dates/) - default_btn = inclusive_dates.find_elements(:link, 'Set as Default') - - if default_btn[0] - default_btn[0].click - # Keep looping until the 'Set as Default' button is gone - @driver.wait_for_ajax - sleep 3 - else - break - end - end + inclusive_dates = @driver.find_element_with_text('//tr', /Inclusive Dates/) + default_btn = inclusive_dates.find_element(:link, 'Set as Default') + @driver.click_and_wait_until_element_gone(default_btn) @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") @driver.find_element(:css => '#accession_dates_ .subrecord-form-heading .btn:not(.show-all)').click @@ -112,8 +108,10 @@ end it "lets you add a new value to an enumeration, reorder it and then you can use it" do + @driver.get($frontend) @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Controlled Value Lists").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Manage Controlled Value Lists") enum_select = @driver.find_element(:id => "enum_selector") enum_select.select_option_with_text("Collection Management Processing Priority (collection_management_processing_priority)") @@ -122,18 +120,14 @@ @driver.find_element(:css, '.enumeration-list') @driver.find_element(:link, 'Create Value').click - @driver.clear_and_send_keys([:id, "enumeration_value_"], "IMPORTANT.\n") + @driver.clear_and_send_keys([:id, "enumeration_value_"], "IMPORTANT.") + @driver.click_and_wait_until_gone(:css, '.modal-footer .btn-primary') @driver.find_element_with_text('//td', /^IMPORTANT\.$/) - # lets move important up the list - 3.times do - @driver.find_element_with_text('//tr', /IMPORTANT/).find_element(:css, '.position-up').click - end - # now lets make sure it's there @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") cm_accession_title = "CM Punk TEST" @driver.clear_and_send_keys([:id, "accession_title_"], cm_accession_title) @@ -145,9 +139,6 @@ #now add collection management @driver.find_element(:css => '#accession_collection_management_ .subrecord-form-heading .btn:not(.show-all)').click - # our new value should be #1! - @driver.find_element(:id => "accession_collection_management__processing_priority_").text.each_line.first.chomp.should eq("IMPORTANT.") - @driver.find_element(:id => "accession_collection_management__processing_priority_").select_option("IMPORTANT.") @driver.click_and_wait_until_gone(:css => "form#accession_form button[type='submit']") @@ -157,21 +148,23 @@ end it "lets you see how many times the term has been used and search for it" do + @driver.get($frontend) @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Controlled Value Lists").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Manage Controlled Value Lists") run_index_round enum_select = @driver.find_element(:id => "enum_selector") enum_select.select_option_with_text("Collection Management Processing Priority (collection_management_processing_priority)") - @driver.wait_for_ajax - @driver.find_element(:link, "1 related item.") - + @driver.find_element(:link, "1 related item.") end it "lets you suppress an enumeration value" do + @driver.get($frontend) @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Controlled Value Lists").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Manage Controlled Value Lists") enum_select = @driver.find_element(:id => "enum_selector") enum_select.select_option_with_text("Collection Management Processing Priority (collection_management_processing_priority)") @@ -179,23 +172,22 @@ # Wait for the table of enumerations to load @driver.find_element(:css, '.enumeration-list') + # Wait for click event to be bound to the Create Value link + @driver.wait_for_dropdown @driver.find_element(:link, 'Create Value').click - @driver.clear_and_send_keys([:id, "enumeration_value_"], "fooman\n") + + @driver.clear_and_send_keys([:id, "enumeration_value_"], "fooman") + @driver.click_and_wait_until_gone(:css, '.modal-footer .btn-primary') foo = @driver.find_element_with_text('//tr', /fooman/) - foo.find_element(:link, "Suppress").click + @driver.click_and_wait_until_element_gone(foo.find_element(:link, "Suppress")) - assert(5) { - @driver.find_element_with_text('//tr', /fooman/).find_element(:link, "Unsuppress").should_not be_nil - } - - assert(5) { - @driver.find_element_with_text('//tr', /fooman/).find_elements(:link, 'Delete').length.should eq(0) - } + @driver.find_element_with_text('//tr', /fooman/).find_element(:link, "Unsuppress").should_not be_nil + @driver.find_element_with_text('//tr', /fooman/).find_elements(:link, 'Delete').length.should eq(0) # now lets make sure it's there @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Accession").click + @driver.click_and_wait_until_gone(:link, "Accession") cm_accession_title = "CM Punk TEST2" @driver.clear_and_send_keys([:id, "accession_title_"], cm_accession_title) diff --git a/selenium/spec/events_spec.rb b/selenium/spec/events_spec.rb index ead6ef59e1..e9401805e4 100644 --- a/selenium/spec/events_spec.rb +++ b/selenium/spec/events_spec.rb @@ -32,7 +32,7 @@ it "creates an event and links it to an agent and an agent as a source" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Event").click + @driver.click_and_wait_until_gone(:link, "Event") @driver.find_element(:id, "event_event_type_").select_option('accession') @driver.find_element(:id, "event_outcome_").select_option("pass") @driver.clear_and_send_keys([:id, "event_outcome_note_"], "OK, that's another lie: all test subjects perished.") @@ -70,23 +70,22 @@ } run_all_indexers - expect { - assert(10) { - @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Agents").click + @driver.get($frontend) + + @driver.find_element(:link, "Browse").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Agents") - @driver.clear_and_send_keys([:css, ".sidebar input.text-filter-field"], "Geddy*" ) - @driver.find_element(:css, ".sidebar input.text-filter-field + div button").click - @driver.find_element_with_text('//tr', /Geddy/).find_element(:link, 'View').click + @driver.clear_and_send_keys([:css, ".sidebar input.text-filter-field"], "Geddy*" ) + @driver.click_and_wait_until_gone(:css, ".sidebar input.text-filter-field + div button") + @driver.click_and_wait_until_element_gone(@driver.find_element_with_text('//tr', /Geddy/).find_element(:link, 'View')) - @driver.find_element_with_text('//td', /accession/) - } - }.not_to raise_error + @driver.find_element_with_text('//td', /accession/) end it "creates an event and links it to an agent and accession" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Event").click + @driver.click_and_wait_until_gone(:link, "Event") @driver.find_element(:id, "event_event_type_").select_option('virus_check') @driver.find_element(:id, "event_outcome_").select_option("pass") @driver.clear_and_send_keys([:id, "event_outcome_note_"], "OK, that's a lie: all test subjects perished.") diff --git a/selenium/spec/groups_spec.rb b/selenium/spec/groups_spec.rb index 8e29df810a..8d4916f9cb 100644 --- a/selenium/spec/groups_spec.rb +++ b/selenium/spec/groups_spec.rb @@ -9,7 +9,7 @@ @user = create_user # wait for notification to fire (which can take up to 5 seconds) - @driver = Driver.get.login($admin) + @driver = Driver.get.login($admin) end @@ -22,20 +22,23 @@ @driver.select_repo(@repo_to_manage) @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Manage Groups").click row = @driver.find_element_with_text('//tr', /repository-archivists/) - row.find_element(:link, 'Edit').click + edit_link = row.find_element(:link, 'Edit') + @driver.click_and_wait_until_element_gone(edit_link) @driver.clear_and_send_keys([:id, 'new-member'],(@user.username)) @driver.find_element(:id, 'add-new-member').click - @driver.find_element(:css => 'button[type="submit"]').click + @driver.click_and_wait_until_gone(:css => 'button[type="submit"]') end it "can assign the test user to the viewers group of the first repository" do @driver.select_repo(@repo_to_view) @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Manage Groups").click row = @driver.find_element_with_text('//tr', /repository-viewers/) @@ -43,19 +46,20 @@ @driver.clear_and_send_keys([:id, 'new-member'],(@user.username)) @driver.find_element(:id, 'add-new-member').click - @driver.find_element(:css => 'button[type="submit"]').click + @driver.click_and_wait_until_gone(:css => 'button[type="submit"]') end it "reports errors when attempting to create a Group with missing data" do @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Manage Groups").click @driver.find_element(:link, "Create Group").click @driver.find_element(:css => "form#new_group button[type='submit']").click expect { @driver.find_element_with_text('//div[contains(@class, "error")]', /Group code - Property is required but was missing/) }.to_not raise_error - @driver.find_element(:link, "Cancel").click + @driver.click_and_wait_until_gone(:link, "Cancel") end @@ -64,7 +68,7 @@ @driver.clear_and_send_keys([:id, 'group_group_code_'], "goo") @driver.clear_and_send_keys([:id, 'group_description_'], "Goo group to group goo") @driver.find_element(:id, "view_repository").click - @driver.find_element(:css => "form#new_group button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_group button[type='submit']") expect { @driver.find_element_with_text('//tr', /goo/) }.to_not raise_error @@ -78,14 +82,16 @@ expect { @driver.find_element_with_text('//div[contains(@class, "error")]', /Description - Property is required but was missing/) }.to_not raise_error - @driver.find_element(:link, "Cancel").click + @driver.click_and_wait_until_gone(:link, "Cancel") end it "can edit a Group" do - @driver.find_element_with_text('//tr', /goo/).find_element(:link, "Edit").click + row = @driver.find_element_with_text('//tr', /goo/) + edit_link = row.find_element(:link, "Edit") + @driver.click_and_wait_until_element_gone(edit_link) @driver.clear_and_send_keys([:id, 'group_description_'], "Group to gather goo") - @driver.find_element(:css => "form#new_group button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_group button[type='submit']") expect { @driver.find_element_with_text('//tr', /Group to gather goo/) }.to_not raise_error @@ -107,7 +113,7 @@ @driver.find_element(:link, "Sign In").click @driver.clear_and_send_keys([:id, 'user_username'], @user.username) @driver.clear_and_send_keys([:id, 'user_password'], @user.password) - @driver.find_element(:id, 'login').click + @driver.click_and_wait_until_gone(:id, 'login') assert(5) { @driver.find_element(:css => "span.user-label").text.should match(/#{@user.username}/) } end @@ -127,6 +133,7 @@ # change @can_manage_repo to a view only @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Manage User Access").click while true @@ -136,7 +143,7 @@ user_row = @driver.find_element_with_text('//tr', /#{@user.username}/, true, true) if user_row - user_row.find_element(:link, "Edit Groups").click + @driver.click_and_wait_until_element_gone(user_row.find_element(:link, "Edit Groups")) break end @@ -158,7 +165,7 @@ # check only the viewer group @driver.find_element_with_text('//tr', /repository-viewers/).find_element(:css, 'input').click - @driver.find_element(:id, "create_account").click + @driver.click_and_wait_until_gone(:id, "create_account") @driver.logout end @@ -176,6 +183,7 @@ # change @can_manage_repo to a view only @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Manage User Access").click while true diff --git a/selenium/spec/instances_and_containers_spec.rb b/selenium/spec/instances_and_containers_spec.rb index a674f238c9..13d3a07598 100644 --- a/selenium/spec/instances_and_containers_spec.rb +++ b/selenium/spec/instances_and_containers_spec.rb @@ -68,6 +68,7 @@ # Now bulk update Letter E's ILD # @driver.find_element(:css => ".bulk-operation-toolbar:first-child a.dropdown-toggle").click + @driver.wait_for_dropdown @driver.find_element(:id => "bulkActionUpdateIlsHolding").click @@ -101,6 +102,7 @@ # Create a top container elt.find_element(:css => 'a.dropdown-toggle').click + @driver.wait_for_dropdown elt.find_element(:css => 'a.linker-create-btn').click modal = @driver.find_element(:css => '#resource_instances__0__sub_container__top_container__ref__modal') @@ -109,6 +111,7 @@ # Create a top container profile within the top container modal.find_element(:css => '.dropdown-toggle.last').click + @driver.wait_for_dropdown modal.find_element(:css, "a.linker-create-btn").click profile_modal = @driver.find_element(:css => '#top_container_container_profile__ref__modal') @@ -133,9 +136,10 @@ elt.find_element(:css => '#top_container_container_locations__0__end_date_').attribute('value').should eq("") } - elt.find_element(:css => '.dropdown-toggle.locations').click + @driver.scroll_into_view(elt.find_element(:css, ".dropdown-toggle.locations")).click + @driver.wait_for_dropdown @driver.wait_for_ajax - elt.find_element(:css, "a.linker-create-btn").click + @driver.scroll_into_view(elt.find_element(:css, "a.linker-create-btn")).click loc_modal = @driver.find_element(:id => 'top_container_container_locations__0__ref__modal') @@ -144,11 +148,11 @@ loc_modal.clear_and_send_keys([:id, "location_room_"], "66 MOO") loc_modal.clear_and_send_keys([:id, "location_coordinate_1_label_"], "Box XYZ") loc_modal.clear_and_send_keys([:id, "location_coordinate_1_indicator_"], "XYZ0001") - loc_modal.click_and_wait_until_gone(:css => "#createAndLinkButton") + + @driver.find_element_with_text('//button', /Create and Link to Location/).click # re-find our original modal - modal = @driver.test_find_element(:css => '#resource_instances__0__sub_container__top_container__ref__modal') - modal.find_element(:id => 'createAndLinkButton').click + @driver.scroll_into_view(@driver.find_element_with_text('//button', /Create and Link to Top Container/)).click @driver.find_element(:css => "form .record-pane button[type='submit']").click @@ -167,6 +171,7 @@ # Create top container elt.find_element(:css => 'a.dropdown-toggle').click + @driver.wait_for_dropdown elt.find_element(:css => 'a.linker-create-btn').click modal = @driver.find_element(:css => '#accession_instances__0__sub_container__top_container__ref__modal') @@ -184,9 +189,9 @@ elt.find_element(:css => '#top_container_container_locations__0__end_date_').attribute('value').should eq("") } - elt.find_element(:css => '.dropdown-toggle.locations').click + @driver.scroll_into_view(elt.find_element(:css, ".dropdown-toggle.locations")).click + @driver.wait_for_dropdown @driver.wait_for_ajax - @driver.scroll_into_view(elt.find_element(:css, "a.linker-create-btn")).click loc_modal = @driver.find_element(:id => 'top_container_container_locations__0__ref__modal') diff --git a/selenium/spec/jobs_spec.rb b/selenium/spec/jobs_spec.rb index 9b345fed4f..fd68227682 100644 --- a/selenium/spec/jobs_spec.rb +++ b/selenium/spec/jobs_spec.rb @@ -27,12 +27,12 @@ run_index_round @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Background Jobs").click @driver.find_element(:link, "Create Job").click - - @driver.find_element(:id => "job_job_type_").select_option("find_and_replace_job") + @driver.click_and_wait_until_gone(:link, 'Batch Find and Replace (Beta)') token_input = @driver.find_element(:id,"token-input-find_and_replace_job_ref_") token_input.clear @@ -61,11 +61,11 @@ run_index_round @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click + @driver.wait_for_dropdown @driver.find_element(:link, "Background Jobs").click @driver.find_element(:link, "Create Job").click - - @driver.find_element(:id => "job_job_type_").select_option("print_to_pdf_job") + @driver.click_and_wait_until_gone(:link, 'Print To PDF') token_input = @driver.find_element(:id,"token-input-print_to_pdf_job_ref_") token_input.clear @@ -81,37 +81,31 @@ end it "can create a report job" do - system("rm -f #{File.join(Dir.tmpdir, '*.csv')}") run_index_round @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click - @driver.find_element(:link, "Background Jobs").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Background Jobs") @driver.find_element(:link, "Create Job").click - - @driver.find_element(:id => "job_job_type_").select_option("report_job") + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, 'Reports') @driver.find_element(:xpath => "//button[@data-report = 'repository_report']").click - sleep(2) - @driver.find_element(:id => "report_job_format").select_option("csv") - @driver.find_element_with_text("//button", /Queue Job/).click - expect { - @driver.find_element_with_text("//h2", /report_job/) - }.to_not raise_error + # wait for the slow fade to finish and all sibling items to be removed + sleep(2) - sleep(5) - @driver.find_element(:link, "Download Report").click - sleep(1) + job_type = @driver.execute_script("return $('#report_job_jsonmodel_type_').val()") + expect(job_type).to eq('report_job') - assert(30) { - glob = Dir.glob(File.join( Dir.tmpdir,"*.csv" )) - raise "Retry assert as CSV file not found" if glob.empty? + report_type = @driver.execute_script("return $('#report_type_').val()") + expect(report_type).to eq('repository_report') - glob.length.should eq(1) - } + @driver.find_element(:id => "report_job_format").select_option("csv") + @driver.click_and_wait_until_element_gone(@driver.find_element_with_text("//button", /Queue Job/)) - IO.read( Dir.glob(File.join( Dir.tmpdir,"*.csv" )).first ).include?(@repo.name) + @driver.find_element_with_text("//h2", /report_job/) end end diff --git a/selenium/spec/locations_spec.rb b/selenium/spec/locations_spec.rb index 57dcd90031..9adac9e948 100644 --- a/selenium/spec/locations_spec.rb +++ b/selenium/spec/locations_spec.rb @@ -20,7 +20,8 @@ it "allows access to the single location form" do @driver.find_element(:link, "Create").click @driver.find_element(:link, "Location").click - @driver.find_element(:link, "Single Location").click + @driver.click_and_wait_until_gone(:link, "Single Location") + @driver.find_element(:css, "h2").text.should eq("New Location Location") end @@ -57,25 +58,36 @@ it "lists the new location in the browse list" do run_index_round + @driver.get($frontend) + @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Locations").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Locations") + @driver.find_paginated_element(:xpath => "//tr[.//*[contains(text(), '129 W. 81st St, 5, 5A [Box XYZ: XYZ0001]')]]") end it "allows the new location to be viewed in non-edit mode" do + @driver.get($frontend) + @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Locations").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Locations") @driver.clear_and_send_keys([:css, ".sidebar input.text-filter-field"], "129*" ) - @driver.find_element(:css, ".sidebar input.text-filter-field + div button").click - @driver.find_element(:link, "Edit").click + @driver.click_and_wait_until_gone(:css, ".sidebar input.text-filter-field + div button") + @driver.click_and_wait_until_gone(:link, "Edit") assert(5) { @driver.find_element(:css, '.record-pane h2').text.should match(/129 W\. 81st St/) } end - + it "allows creation of a location with plus one stickies" do + @driver.get($frontend) + @driver.find_element(:link, "Create").click + @driver.wait_for_dropdown @driver.find_element(:link, "Location").click - @driver.find_element(:link, "Single Location").click + + @driver.click_and_wait_until_gone(:link, "Single Location") @driver.clear_and_send_keys([:id, "location_building_"], "123 Fake St") @driver.clear_and_send_keys([:id, "location_floor_"], "13") @driver.clear_and_send_keys([:id, "location_room_"], "237") @@ -110,8 +122,8 @@ @driver.logout.login(@archivist_user) @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Locations").click - + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Locations") @driver.find_paginated_element(:xpath => "//tr[.//*[contains(text(), '129 W. 81st St, 5, 5A [Box XYZ: XYZ0001]')]]") end @@ -124,7 +136,7 @@ @driver.ensure_no_such_element(:link, "Edit") } - @driver.find_element(:link, "View").click + @driver.click_and_wait_until_gone(:link, "View") assert(20) { @driver.ensure_no_such_element(:link, "Edit") @@ -132,7 +144,7 @@ end - it "lists the location in different repositories", :retry => 2, :retry_wait => 10 do + it "lists the location in different repositories" do repo = create(:repo) @driver.logout.login($admin) @@ -143,7 +155,8 @@ } @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Locations").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Locations") expect { @driver.find_paginated_element(:xpath => "//tr[.//*[contains(text(), '129 W. 81st St, 5, 5A [Box XYZ: XYZ0001]')]]") @@ -157,9 +170,12 @@ end it "displays error messages upon invalid batch" do + @driver.get($frontend) + @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Locations").click - @driver.find_element(:link, "Create Batch Locations").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Locations") + @driver.click_and_wait_until_gone(:link, "Create Batch Locations") @driver.click_and_wait_until_gone(:css => "form#new_location_batch .btn-primary") @@ -187,7 +203,7 @@ @driver.clear_and_send_keys([:id, "location_batch_coordinate_2_range__start_"], "1") @driver.clear_and_send_keys([:id, "location_batch_coordinate_2_range__end_"], "4") - @driver.click_and_wait_until_gone(:css => "form#new_location_batch .btn.preview-locations") + @driver.find_element(:css => "form#new_location_batch .btn.preview-locations").click modal = @driver.find_element(:id, "batchPreviewModal") @driver.wait_for_ajax @@ -213,7 +229,7 @@ @driver.navigate.refresh @driver.clear_and_send_keys([:css, ".sidebar input.text-filter-field"], "1978*" ) - @driver.find_element(:css, ".sidebar input.text-filter-field + div button").click + @driver.click_and_wait_until_gone(:css, ".sidebar input.text-filter-field + div button") @driver.find_element_with_text('//td', /1978 Awesome Street \[Room: 1A, Shelf: 1\]/) @@ -231,19 +247,15 @@ @driver.logout.login($admin) @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Locations").click + @driver.click_and_wait_until_gone(:link, "Locations") @driver.clear_and_send_keys([:css, ".sidebar input.text-filter-field"], "1978*" ) - @driver.find_element(:css, ".sidebar input.text-filter-field + div button").click + @driver.click_and_wait_until_gone(:css, ".sidebar input.text-filter-field + div button") - - - @driver.blocking_find_elements(:css, ".multiselect-column input").slice(0..7).each do |checkbox| - checkbox.click + (0..7).each do |i| + @driver.execute_script("$($('.multiselect-column input').get(#{i})).click()") end - - @driver.find_element(:css, ".record-toolbar .btn.multiselect-enabled.edit-batch").click @driver.find_element(:css, "#confirmChangesModal #confirmButton").click @@ -256,7 +268,7 @@ run_index_round @driver.navigate.refresh @driver.clear_and_send_keys([:css, ".sidebar input.text-filter-field"], "1978*") - @driver.find_element(:css, ".sidebar input.text-filter-field + div button").click + @driver.click_and_wait_until_gone(:css, ".sidebar input.text-filter-field + div button") @driver.find_element_with_text('//td', /1978 Awesome Street, 6th, Studio 5, The corner \[Room: 1A, Shelf: 1\]/) @driver.find_element_with_text('//td', /1978 Awesome Street, 6th, Studio 5, The corner \[Room: 1A, Shelf: 2\]/) @@ -272,9 +284,7 @@ it "can create locations with +1 stickyness" do @driver.navigate.to("#{$frontend}/locations") - @driver.find_element(:link, "Create Batch Locations").click - - @driver.click_and_wait_until_gone(:css => "form#new_location_batch .btn-primary") + @driver.click_and_wait_until_gone(:link, "Create Batch Locations") @driver.clear_and_send_keys([:id, "location_batch_building_"], "555 Fake Street") @driver.clear_and_send_keys([:id, "location_batch_floor_"], "2nd") @@ -287,8 +297,6 @@ @driver.clear_and_send_keys([:id, "location_batch_coordinate_2_range__start_"], "1") @driver.clear_and_send_keys([:id, "location_batch_coordinate_2_range__end_"], "4") - @driver.wait_for_ajax - @driver.click_and_wait_until_gone(:css => "form#new_location_batch .createPlusOneBtn") @driver.find_element_with_text('//div[contains(@class, "alert-success")]', /Locations Created/) diff --git a/selenium/spec/merge_and_transfer_spec.rb b/selenium/spec/merge_and_transfer_spec.rb index 6ac6ffc237..1afad65559 100644 --- a/selenium/spec/merge_and_transfer_spec.rb +++ b/selenium/spec/merge_and_transfer_spec.rb @@ -34,6 +34,8 @@ @driver.find_element(:id, "transfer_ref_").select_option_with_text(@target_repo.repo_code) @driver.find_element(:css => ".transfer-button").click @driver.find_element(:css, "#confirmButton").click + @driver.wait_for_ajax + @driver.find_element_with_text('//div[contains(@class, "alert-success")]', /Transfer Successful/) run_all_indexers @@ -41,7 +43,7 @@ @driver.select_repo(@target_repo) @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Resources").click + @driver.click_and_wait_until_gone(:link, "Resources") @driver.find_element(:xpath => "//td[contains(text(), '#{@resource.title}')]") @@ -55,6 +57,7 @@ @driver.get_edit_page(@resource2) @driver.find_element(:link, "Merge").click + @driver.wait_for_ajax # spaces in the search string seem to through off the token search, so: search_string = @resource3.title.sub(/-\s.*/, "").strip @@ -64,11 +67,11 @@ @driver.find_element(:css, "button.merge-button").click @driver.find_element_with_text("//h3", /Merge into this record\?/) - @driver.find_element(:css, "button#confirmButton").click + @driver.click_and_wait_until_gone(:css, "button#confirmButton") (@aoset2 + @aoset3).each do |ao| assert(5) { - @driver.find_element(:id => js_node(ao).li_id) + @driver.find_element(:id => tree_node(ao).tree_id) } end end @@ -87,15 +90,17 @@ @driver.find_element(:css, "li.token-input-dropdown-item2").click @driver.find_element(:css, "button.transfer-button").click + @driver.wait_for_ajax @driver.find_element_with_text('//div[contains(@class, "alert-success")]', /Successfully transferred Archival Object/) - @driver.wait_for_ajax @driver.get_edit_page(@resource2) + @driver.wait_for_ajax + (@aoset2 + @aoset3).each do |ao| assert(5) { - @driver.find_element(:id => js_node(ao).li_id) + @driver.find_element(:id => tree_node(ao).tree_id) } end end diff --git a/selenium/spec/notes_spec.rb b/selenium/spec/notes_spec.rb index 36bfa54458..d7e3ce9053 100644 --- a/selenium/spec/notes_spec.rb +++ b/selenium/spec/notes_spec.rb @@ -18,7 +18,7 @@ after(:all) do - @driver.logout.quit + @driver.quit end @@ -119,7 +119,7 @@ end - it "can add a top-level bibliography too", :retry => 2, :retry_wait => 10 do + it "can add a top-level bibliography too" do @driver.get_edit_page(@resource) @@ -157,7 +157,7 @@ @driver.execute_script("$('#resource_notes__0__subnotes__0__content_').data('CodeMirror').setSelection({line: 0, ch: 0}, {line: 0, ch: 3})") # select a tag to wrap the text - assert(5) { @driver.find_element(:css => "select.mixed-content-wrap-action").select_option("blockquote") } + @driver.find_element(:css => "select.mixed-content-wrap-action").select_option("blockquote") @driver.execute_script("$('#resource_notes__0__subnotes__0__content_').data('CodeMirror').save()") @driver.execute_script("$('#resource_notes__0__subnotes__0__content_').data('CodeMirror').toTextArea()") @driver.find_element(:id => "resource_notes__0__subnotes__0__content_").attribute("value").should eq("
      ABC
      ") @@ -168,7 +168,7 @@ end - it "can add a deaccession record", :retry => 2, :retry_wait => 10 do + it "can add a deaccession record" do @driver.get_edit_page(@resource) @driver.find_element(:css => '#resource_deaccessions_ .subrecord-form-heading .btn:not(.show-all)').click @@ -187,11 +187,11 @@ end - it "can attach notes to archival objects", :retry => 2, :retry_wait => 10 do + it "can attach notes to archival objects" do @driver.navigate.to("#{$frontend}") # Create a resource @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Resource").click + @driver.click_and_wait_until_gone(:link, "Resource") @driver.clear_and_send_keys([:id, "resource_title_"], "a resource") @driver.complete_4part_id("resource_id_%d_") @@ -207,7 +207,7 @@ @driver.find_element(:id => "resource_dates__0__date_type_").select_option("single") @driver.clear_and_send_keys([:id, "resource_dates__0__begin_"], "1978") - @driver.find_element(:css => "form#resource_form button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#resource_form button[type='submit']") # Give it a child AO @driver.click_and_wait_until_gone(:link, "Add Child") @@ -230,19 +230,15 @@ @driver.blocking_find_elements(:css => '#notes > .subrecord-form-container > .subrecord-form-list > li').length.should eq(3) - @driver.find_element(:link, "Revert Changes").click - - - # Skip over "Save Your Changes" dialog i.e. don't save AO. - @driver.find_element(:id, "dismissChangesButton").click + @driver.click_and_wait_until_gone(:css => '.btn.btn-cancel.btn-default') end - it "can attach special notes to digital objects", :retry => 2, :retry_wait => 10 do + it "can attach special notes to digital objects" do @driver.navigate.to("#{$frontend}") @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Digital Object").click + @driver.click_and_wait_until_gone(:link, "Digital Object") @driver.clear_and_send_keys([:id, "digital_object_title_"], "A digital object with notes") @driver.clear_and_send_keys([:id, "digital_object_digital_object_id_"],(Digest::MD5.hexdigest("#{Time.now}"))) @@ -258,7 +254,7 @@ @driver.execute_script("$('#digital_object_notes__0__content__0_').data('CodeMirror').toTextArea()") @driver.find_element(:id => "digital_object_notes__0__content__0_").attribute("value").should eq("Summary content") - @driver.find_element(:css => "form#new_digital_object button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_digital_object button[type='submit']") end end diff --git a/selenium/spec/pagination_spec.rb b/selenium/spec/pagination_spec.rb index 3729027914..7769afc8aa 100644 --- a/selenium/spec/pagination_spec.rb +++ b/selenium/spec/pagination_spec.rb @@ -29,12 +29,12 @@ it "can navigate through pages of accessions" do @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Accessions").click + @driver.click_and_wait_until_gone(:link, "Accessions") expect { @driver.find_element_with_text('//div', /Showing 1 - #{AppConfig[:default_page_size]}/) }.to_not raise_error - @driver.find_element(:xpath, '//a[@title="Next"]').click + @driver.click_and_wait_until_gone(:xpath, '//a[@title="Next"]') expect { @driver.find_element_with_text('//div', /Showing #{AppConfig[:default_page_size] + 1}/) }.to_not raise_error @@ -42,12 +42,12 @@ it "can navigate through pages of digital objects " do @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Digital Objects").click + @driver.click_and_wait_until_gone(:link, "Digital Objects") expect { @driver.find_element_with_text('//div', /Showing 1 - #{AppConfig[:default_page_size]}/) }.to_not raise_error - @driver.find_element(:xpath, '//a[@title="Next"]').click + @driver.click_and_wait_until_gone(:xpath, '//a[@title="Next"]') expect { @driver.find_element_with_text('//div', /Showing #{AppConfig[:default_page_size] + 1}/) }.to_not raise_error diff --git a/selenium/spec/parallel_formatter_out.rb b/selenium/spec/parallel_formatter_out.rb index 11f1cc013b..9f701fb5bd 100644 --- a/selenium/spec/parallel_formatter_out.rb +++ b/selenium/spec/parallel_formatter_out.rb @@ -37,7 +37,7 @@ def passed_output(example) thread_id + " " + RSpec::Core::Formatters::ConsoleCodes.wrap("#{example.metadata[:example_group][:full_description].strip}: #{example.description.strip}", :success) end - def failure_output(example, _exception) + def failure_output(example) thread_id + " " + RSpec::Core::Formatters::ConsoleCodes.wrap("#{example.metadata[:example_group][:full_description].strip}: #{example.description.strip} " "(FAILED - #{next_failure_index})", :failure) end diff --git a/selenium/spec/permissions_spec.rb b/selenium/spec/permissions_spec.rb index b8119fcf50..56549aff7a 100644 --- a/selenium/spec/permissions_spec.rb +++ b/selenium/spec/permissions_spec.rb @@ -12,18 +12,18 @@ after(:all) do - @driver.logout.quit + @driver.quit end it "allows archivists to edit major record types by default" do @driver.login_to_repo(@archivist, @repo) @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Accession').click + @driver.click_and_wait_until_gone(:link => 'Accession') @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Resource').click + @driver.click_and_wait_until_gone(:link => 'Resource') @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Digital Object').click + @driver.click_and_wait_until_gone(:link => 'Digital Object') @driver.logout end @@ -31,12 +31,13 @@ it "supports denying permission to edit Resources" do @driver.login_to_repo($admin, @repo) @driver.find_element(:css, '.repo-container .btn.dropdown-toggle').click - @driver.find_element(:link, "Manage Groups").click + @driver.wait_for_dropdown + @driver.click_and_wait_until_gone(:link, "Manage Groups") row = @driver.find_element_with_text('//tr', /repository-archivists/) - row.find_element(:link, 'Edit').click + row.click_and_wait_until_gone(:link, 'Edit') @driver.find_element(:xpath, '//input[@id="update_resource_record"]').click - @driver.find_element(:css => 'button[type="submit"]').click + @driver.click_and_wait_until_gone(:css => 'button[type="submit"]') @driver.login_to_repo(@archivist, @repo) @driver.find_element(:link => 'Create').click @driver.ensure_no_such_element(:link, "Resource") diff --git a/selenium/spec/rde_spec.rb b/selenium/spec/rde_spec.rb index 6577404d05..82ce2333d9 100644 --- a/selenium/spec/rde_spec.rb +++ b/selenium/spec/rde_spec.rb @@ -70,16 +70,14 @@ @driver.click_and_wait_until_gone(:css => ".modal-footer .btn-primary") - @driver.wait_for_ajax - expect { - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /My AO, 2013/) - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /Item/) + tree_node_for_title('My AO, 2013') + tree_node_for_title('Item') }.not_to raise_error end it "can access the RDE form when editing an archival object" do - @driver.find_element(:css, "#archives_tree_toolbar .btn-next-tree-node").click + @driver.find_element(:css, "tr.largetree-node.indent-level-1 a.record-title").click @driver.wait_for_ajax expect { @@ -120,9 +118,10 @@ @driver.click_and_wait_until_gone(:css => ".modal-footer .btn-primary") @driver.wait_for_ajax + assert(5) { - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /Child 1, 2013/) - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /Child 2, 2013/) + tree_node_for_title('Child 1, 2013') + tree_node_for_title('Child 2, 2013') } end @@ -177,7 +176,7 @@ modal.find_element(:css, ".btn.fill-column").click modal.find_element(:id, "basicFillTargetColumn").select_option("colLevel") modal.find_element(:id, "basicFillValue").select_option("item") - @driver.click_and_wait_until_gone(:css, "#fill_basic .btn-primary") + @driver.find_element(:css, "#fill_basic .btn-primary").click # all should have item as the level modal.find_element(:id, "archival_record_children_children__0__level_").get_select_value.should eq("item") @@ -192,7 +191,7 @@ modal.find_element(:id, "archival_record_children_children__9__level_").get_select_value.should eq("item") end - it "can perform a sequence fill", :retry => 2, :retry_wait => 10 do + it "can perform a sequence fill" do modal = @driver.find_element(:id => "rapidDataEntryModal") modal.find_element(:css, ".btn.fill-column").click @@ -202,13 +201,13 @@ @driver.clear_and_send_keys([:id, "sequenceFillPrefix"], "ABC") @driver.clear_and_send_keys([:id, "sequenceFillFrom"], "1") @driver.clear_and_send_keys([:id, "sequenceFillTo"], "5") - @driver.click_and_wait_until_gone(:css, "#fill_sequence .btn-primary") + @driver.find_element(:css, "#fill_sequence .btn-primary").click # message should be displayed "not enough in the sequence" or thereabouts.. modal.find_element(:id, "sequenceTooSmallMsg") @driver.clear_and_send_keys([:id, "sequenceFillTo"], "10") - @driver.click_and_wait_until_gone(:css, "#fill_sequence .btn-primary") + @driver.find_element(:css, "#fill_sequence .btn-primary").click # check the component id for each row matches the sequence modal.find_element(:id, "archival_record_children_children__0__component_id_").attribute("value").should eq("ABC1") @@ -226,40 +225,20 @@ it "can perform a column reorder" do modal = @driver.find_element(:id => "rapidDataEntryModal") - modal.find_element(:css, ".btn.reorder-columns").click + old_position = modal.find_elements(:css, "table .fieldset-labels th").index {|cell| cell.attribute("id") === "colLevel"} - # move Note Type 1 to the first position - modal.find_element(:id, "columnOrder").select_option("colNType1") - 24.times { modal.find_element(:id, "columnOrderUp").click } + modal.find_element(:css, ".btn.reorder-columns").click - # move Instance Type to the second position - modal.find_element(:id, "columnOrder").select_option("colNType1") # deselect Note Type 1 - modal.find_element(:id, "columnOrder").select_option("colIType") - 16.times { modal.find_element(:id, "columnOrderUp").click } + # Move Level Of Description down + @driver.find_element(:css, '#rapidDataEntryModal #columnOrder').select_option("colLevel") + modal.find_element(:id, "columnOrderDown").click # apply the new order @driver.click_and_wait_until_gone(:css, "#columnReorderForm .btn-primary") - # check the first few headers now match the new order - cells = modal.find_elements(:css, "table .fieldset-labels th") - cells[2].attribute("id").should eq("colNType1") - cells[3].attribute("id").should eq("colIType") - cells[4].attribute("id").should eq("colOtherLevel") - - # check the section headers are correct - cells = modal.find_elements(:css, "table .sections th") - cells[2].text.should eq("Notes") - cells[2].attribute("colspan").should eq("1") - cells[3].text.should eq("Instance") - cells[3].attribute("colspan").should eq("1") - cells[4].text.should eq("Basic Information") - cells[4].attribute("colspan").should eq("5") - - # check the form fields match the headers - cells = modal.find_elements(:css, "table tbody tr:first-child td") - cells[2].find_element(:id, "archival_record_children_children__0__notes__0__type_") - cells[3].find_element(:id, "archival_record_children_children__0__instances__0__instance_type_") - cells[4].find_element(:id, "archival_record_children_children__0__other_level_") + new_position = modal.find_elements(:css, "table .fieldset-labels th").index {|cell| cell.attribute("id") === "colLevel"} + + old_position.should be < new_position end end @@ -334,13 +313,14 @@ @driver.wait_for_ajax + assert(5) { - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /My DO/) + tree_node_for_title('My DO') } end it "can access the RDE form when editing an digital object" do - @driver.find_element(:css, "#archives_tree_toolbar .btn-next-tree-node").click + @driver.find_element(:css, "tr.largetree-node.indent-level-1 a.record-title").click @driver.wait_for_ajax @driver.find_element(:id, "digital_object_component_title_") @@ -364,8 +344,8 @@ @driver.wait_for_ajax assert(5) { - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /Child 1/) - @driver.find_element_with_text("//div[@id='archives_tree']//li//span", /Child 2/) + tree_node_for_title('Child 1') + tree_node_for_title('Child 2') } end @@ -411,7 +391,7 @@ modal.find_element(:css, ".btn.fill-column").click modal.find_element(:id, "basicFillTargetColumn").select_option("colLabel") @driver.clear_and_send_keys([:id, "basicFillValue"], "NEW_LABEL") - @driver.click_and_wait_until_gone(:css, "#fill_basic .btn-primary") + @driver.find_element(:css, "#fill_basic .btn-primary").click # all should have item as the level assert { @@ -438,13 +418,13 @@ @driver.clear_and_send_keys([:id, "sequenceFillPrefix"], "ABC") @driver.clear_and_send_keys([:id, "sequenceFillFrom"], "1") @driver.clear_and_send_keys([:id, "sequenceFillTo"], "5") - @driver.click_and_wait_until_gone(:css, "#fill_sequence .btn-primary") + @driver.find_element(:css, "#fill_sequence .btn-primary").click # message should be displayed "not enough in the sequence" or thereabouts.. modal.find_element(:id, "sequenceTooSmallMsg") @driver.clear_and_send_keys([:id, "sequenceFillTo"], "10") - @driver.click_and_wait_until_gone(:css, "#fill_sequence .btn-primary") + @driver.find_element(:css, "#fill_sequence .btn-primary").click @driver.wait_for_ajax @@ -462,43 +442,4 @@ end - it "can perform a column reorder" do - modal = @driver.find_element(:id => "rapidDataEntryModal") - - modal.find_element(:css, ".btn.reorder-columns").click - - # move Note Type 1 to the first position - modal.find_element(:id, "columnOrder").select_option("colNType1") - 26.times { modal.find_element(:id, "columnOrderUp").click } - - # move Instance Type to the second position - modal.find_element(:id, "columnOrder").select_option("colNType1") # deselect Note Type 1 - modal.find_element(:id, "columnOrder").select_option("colFUri") - 16.times { modal.find_element(:id, "columnOrderUp").click } - - # apply the new order - @driver.click_and_wait_until_gone(:css, "#columnReorderForm .btn-primary") - - # check the first few headers now match the new order - cells = modal.find_elements(:css, "table .fieldset-labels th") - cells[1].attribute("id").should eq("colNType1") - cells[2].attribute("id").should eq("colFUri") - cells[3].attribute("id").should eq("colLabel") - - # check the section headers are correct - cells = modal.find_elements(:css, "table .sections th") - cells[1].text.should eq("Notes") - cells[1].attribute("colspan").should eq("1") - cells[2].text.should eq("File Version") - cells[2].attribute("colspan").should eq("1") - cells[3].text.should eq("Basic Information") - cells[3].attribute("colspan").should eq("5") - - # check the form fields match the headers - cells = modal.find_elements(:css, "table tbody tr:first-child td") - cells[1].find_element(:id, "digital_record_children_children__0__notes__0__type_") - cells[2].find_element(:id, "digital_record_children_children__0__file_versions__0__file_uri_") - cells[3].find_element(:id, "digital_record_children_children__0__label_") - end - end diff --git a/selenium/spec/record_lifecycle_spec.rb b/selenium/spec/record_lifecycle_spec.rb index 4fb533bc9f..8ff3affffa 100644 --- a/selenium/spec/record_lifecycle_spec.rb +++ b/selenium/spec/record_lifecycle_spec.rb @@ -39,12 +39,12 @@ # make sure we can see suppressed records @driver.find_element(:css, '.user-container .btn.dropdown-toggle.last').click - @driver.find_element(:link, "My Repository Preferences").click + @driver.click_and_wait_until_gone(:link, "My Repository Preferences") elt = @driver.find_element(:xpath, '//input[@id="preference_defaults__show_suppressed_"]') - unless elt[@checked] + unless elt.attribute('checked') elt.click - @driver.find_element(:css => 'button[type="submit"]').click + @driver.click_and_wait_until_gone(:css => 'button[type="submit"]') end # Navigate to the Accession @@ -52,7 +52,7 @@ # Suppress the Accession @driver.find_element(:css, ".suppress-record.btn").click - @driver.find_element(:css, "#confirmChangesModal #confirmButton").click + @driver.click_and_wait_until_gone(:css, "#confirmChangesModal #confirmButton") assert(5) { @driver.find_element(:css => "div.alert.alert-success").text.should eq("Accession #{@accession.title} suppressed") } assert(5) { @driver.find_element(:css => "div.alert.alert-info").text.should eq('Accession is suppressed and cannot be edited') } @@ -69,7 +69,7 @@ @driver.login_to_repo(@archivist_user, @repo) # check the listing @driver.find_element(:link, "Browse").click - @driver.find_element(:link, "Accessions").click + @driver.click_and_wait_until_gone(:link, "Accessions") @driver.find_element_with_text('//h2', /Accessions/) @@ -92,7 +92,7 @@ # Unsuppress the Accession @driver.find_element(:css, ".unsuppress-record.btn").click - @driver.find_element(:css, "#confirmChangesModal #confirmButton").click + @driver.click_and_wait_until_gone(:css, "#confirmChangesModal #confirmButton") assert(5) { @driver.find_element(:css => "div.alert.alert-success").text.should eq("Accession #{@accession.title} unsuppressed") } end @@ -105,7 +105,7 @@ # Delete the accession @driver.find_element(:css, ".delete-record.btn").click - @driver.find_element(:css, "#confirmChangesModal #confirmButton").click + @driver.click_and_wait_until_gone(:css, "#confirmChangesModal #confirmButton") #Ensure Accession no longer exists assert(5) { @driver.find_element(:css => "div.alert.alert-success").text.should eq("Accession #{@accession.title} deleted") } @@ -135,7 +135,7 @@ # Suppress the Digital Object @driver.find_element(:css, ".suppress-record.btn").click - @driver.find_element(:css, "#confirmChangesModal #confirmButton").click + @driver.click_and_wait_until_gone(:css, "#confirmChangesModal #confirmButton") assert(5) { @driver.find_element(:css => "div.alert.alert-success").text.should eq("Digital Object #{@do.title} suppressed") } assert(5) { @driver.find_element(:css => "div.alert.alert-info").text.should eq('Digital Object is suppressed and cannot be edited') } diff --git a/selenium/spec/repositories_spec.rb b/selenium/spec/repositories_spec.rb index cead47c8e3..f1ef170ac3 100644 --- a/selenium/spec/repositories_spec.rb +++ b/selenium/spec/repositories_spec.rb @@ -18,10 +18,10 @@ it "flags errors when creating a repository with missing fields" do @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Repositories").click - @driver.find_element(:link, "Create Repository").click + @driver.click_and_wait_until_gone(:link, "Manage Repositories") + @driver.click_and_wait_until_gone(:link, "Create Repository") @driver.clear_and_send_keys([:id, "repository_repository__name_"], "missing repo code") - @driver.find_element(:css => "form#new_repository button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_repository button[type='submit']") assert(5) { @driver.find_element(:css => "div.alert.alert-danger").text.should eq('Repository Short Name - Property is required but was missing') } end @@ -30,11 +30,11 @@ it "can create a repository" do @driver.clear_and_send_keys([:id, "repository_repository__repo_code_"], @test_repo_code_1) @driver.clear_and_send_keys([:id, "repository_repository__name_"], @test_repo_name_1) - @driver.find_element(:css => "form#new_repository button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_repository button[type='submit']") end it "can add telephone numbers" do - @driver.find_element(:link, 'Edit').click + @driver.click_and_wait_until_gone(:link, 'Edit') @driver.find_element_with_text('//button', /Add Telephone Number/).click @@ -65,10 +65,9 @@ it "Cannot delete the currently selected repository" do run_index_round @driver.select_repo(@test_repo_code_1) - @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Repositories").click + @driver.get("#{$frontend}/repositories") row = @driver.find_paginated_element(:xpath => "//tr[.//*[contains(text(), 'Selected')]]") - row.find_element(:link, 'Edit').click + row.click_and_wait_until_gone(:link, 'Edit') @driver.ensure_no_such_element(:css, "button.delete-record") end @@ -84,10 +83,9 @@ run_all_indexers - @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Repositories").click + @driver.get("#{$frontend}/repositories") row = @driver.find_paginated_element(:xpath => "//tr[.//*[contains(text(), '#{@deletable_repo.repo_code}')]]") - row.find_element(:link, 'Edit').click + @driver.click_and_wait_until_element_gone(row.find_element(:link, 'Edit')) @driver.find_element(:css, ".delete-record.btn").click @driver.clear_and_send_keys([:id, 'deleteRepoConfim'], @deletable_repo.repo_code ) @@ -99,43 +97,48 @@ it "can create a second repository" do - @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Repositories").click - @driver.find_element(:link, "Create Repository").click + @driver.get("#{$frontend}/repositories") + @driver.click_and_wait_until_gone(:link, "Create Repository") @driver.clear_and_send_keys([:id, "repository_repository__repo_code_"], @test_repo_code_2) @driver.clear_and_send_keys([:id, "repository_repository__name_"], @test_repo_name_2) - @driver.find_element(:css => "form#new_repository button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#new_repository button[type='submit']") end it "can select either of the created repositories" do @driver.find_element(:link, 'Select Repository').click - @driver.find_element(:css, '.select-a-repository').find_element(:id => "id").select_option_with_text(@test_repo_code_2) - @driver.find_element(:css, '.select-a-repository .btn-primary').click - assert(5) { @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_2 } + @driver.find_element(:css, '.select-a-repository select').select_option_with_text(@test_repo_code_2) + @driver.click_and_wait_until_gone(:css, '.select-a-repository .btn-primary') + @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_2 @driver.find_element(:link, 'Select Repository').click - @driver.find_element(:css, '.select-a-repository select').find_element(:id => "id").select_option_with_text(@test_repo_code_1) - @driver.find_element(:css, '.select-a-repository .btn-primary').click - assert(5) { @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_1 } + @driver.find_element(:css, '.select-a-repository select').select_option_with_text(@test_repo_code_1) + @driver.click_and_wait_until_gone(:css, '.select-a-repository .btn-primary') + @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_1 @driver.find_element(:link, 'Select Repository').click - @driver.find_element(:css, '.select-a-repository select').find_element(:id => "id").select_option_with_text(@test_repo_code_2) - @driver.find_element(:css, '.select-a-repository .btn-primary').click - assert(5) { @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_2 } - end - - it "will persist repository selection" do - assert(5) { @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_2 } + @driver.find_element(:css, '.select-a-repository select').select_option_with_text(@test_repo_code_2) + @driver.click_and_wait_until_gone(:css, '.select-a-repository .btn-primary') + @driver.find_element(:css, 'span.current-repository-id').text.should eq @test_repo_code_2 end it "automatically refreshes the repository list when a new repo gets added" do repo = create(:repo) + success = false - assert(5) { + Selenium::Config.retries.times do |try| @driver.navigate.refresh + @driver.find_element(:link, 'Select Repository').click - @driver.find_element(:css, '.select-a-repository').select_option_with_text(repo.repo_code) - } + res = @driver.execute_script("return $('option').filter(function (i, elt) { return $(elt).text() == '#{repo.repo_code}' }).length;") + + if res == 1 + success = true + break + end + end + + success.should eq(true) end + end diff --git a/selenium/spec/resources_spec.rb b/selenium/spec/resources_spec.rb index 2d40c406c8..4d187cf2d4 100644 --- a/selenium/spec/resources_spec.rb +++ b/selenium/spec/resources_spec.rb @@ -55,7 +55,7 @@ notes_toggle[0].click @driver.wait_for_ajax - @driver.find_element_orig(:css, '#resource_notes__0__subnotes__0__content_').wait_for_class("initialised"); + @driver.find_element_orig(:css, '#resource_notes__0__subnotes__0__content_').wait_for_class("initialised") @driver.execute_script("$('#resource_notes__0__subnotes__0__content_').data('CodeMirror').toTextArea()") assert(5) { @driver.find_element(:id => "resource_notes__0__subnotes__0__content_").attribute("value").should eq(@accession.content_description) } @@ -69,22 +69,16 @@ @driver.clear_and_send_keys([:id, "resource_extents__0__number_"], "10") @driver.find_element(:id => "resource_extents__0__extent_type_").select_option("cassettes") - - @driver.find_element(:id => "resource_dates__0__date_type_").select_option("single") - @driver.clear_and_send_keys([:id, "resource_dates__0__begin_"], "1978") - - @driver.find_element(:css => "form#resource_form button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form#resource_form button[type='submit']") # Success! - assert(5) { - @driver.find_element_with_text('//div', /Resource .* created/).should_not be_nil - @driver.find_element(:id, "resource_dates__0__begin_" ).attribute("value").should eq("1978") - } + @driver.find_element_with_text('//div', /Resource .* created/).should_not be_nil + @driver.find_element(:id, "resource_dates__0__begin_" ).attribute("value").should eq("1978") end it "reports errors and warnings when creating an invalid Resource" do @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Resource").click + @driver.click_and_wait_until_gone(:link, "Resource") @driver.find_element(:id, "resource_title_").clear @driver.find_element(:css => "form#resource_form button[type='submit']").click @@ -94,7 +88,7 @@ @driver.find_element_with_text('//div[contains(@class, "error")]', /Type - Property is required but was missing/) @driver.find_element_with_text('//div[contains(@class, "warning")]', /Language - Property was missing/) - @driver.find_element(:css, "a.btn.btn-cancel").click + @driver.click_and_wait_until_gone(:css, "a.btn.btn-cancel") end @@ -104,7 +98,7 @@ resource_regex = /^.*?\bPony\b.*?$/m @driver.find_element(:link, "Create").click - @driver.find_element(:link, "Resource").click + @driver.click_and_wait_until_gone(:link, "Resource") @driver.clear_and_send_keys([:id, "resource_title_"],(resource_title)) @driver.complete_4part_id("resource_id_%d_") @@ -123,7 +117,7 @@ @driver.find_element(:css => "form#resource_form button[type='submit']").click # The new Resource shows up on the tree - assert(5) { @driver.find_element(:css => "a.jstree-clicked").text.strip.should match(resource_regex) } + assert(5) { tree_current.text.strip.should match(resource_regex) } end @@ -144,6 +138,8 @@ expect { @driver.find_element_with_text('//div[contains(@class, "error")]', /Title - Property is required but was missing/) }.to_not raise_error + + @driver.click_and_wait_until_gone(:css, "a.btn.btn-cancel") end @@ -163,9 +159,10 @@ @driver.find_element_with_text('//div[contains(@class, "error")]', /Level of Description - Property is required but was missing/) + # click on another node + tree_click(tree_node(@resource)) - @driver.find_element(:link, "Revert Changes").click - @driver.find_element(:id, "dismissChangesButton").click + @driver.click_and_wait_until_gone(:id, "dismissChangesButton") end @@ -184,38 +181,45 @@ @driver.find_element_with_text('//div[contains(@class, "error")]', /Title - must not be an empty string \(or enter a Date\)/i) - @driver.find_element(:link, "Revert Changes").click - @driver.find_element(:id, "dismissChangesButton").click + tree_click(tree_node(@resource)) + @driver.click_and_wait_until_gone(:id, "dismissChangesButton") end - + it "can create a new digital object instance with a note to a resource" do @driver.get_edit_page(@resource) - sleep(1) - @driver.find_element_with_text('//button', /Add Digital Object/).click - sleep(1) + # Wait for the form to load in + @driver.find_element(:css => "form#resource_form button[type='submit']") + @driver.find_element(:css => '#resource_instances_ .subrecord-form-heading .btn[data-instance-type="digital-instance"]').click - @driver.find_element(:css => "#resource_instances_ .linker-wrapper a.btn").click - @driver.find_element(:css => "#resource_instances_ a.linker-create-btn").click - - @driver.clear_and_send_keys([:id, "digital_object_title_"],("digital_object_title")) - @driver.clear_and_send_keys([:id, "digital_object_digital_object_id_"],(Digest::MD5.hexdigest("#{Time.now}"))) + # Wait for the linker to initialise to make sure the dropdown click events are bound + @driver.find_hidden_element(:css => '#resource_instances__0__digital_object__ref_.initialised') + + elt = @driver.find_element(:css => "div[data-id-path='resource_instances__0__digital_object_']") - @driver.find_element(:css => '#digital_object_notes .subrecord-form-heading .btn.add-note').click - @driver.find_last_element(:css => '#digital_object_notes select.top-level-note-type').select_option_with_text("Summary") + elt.find_element(:css => 'a.dropdown-toggle').click + @driver.wait_for_dropdown + elt.find_element(:css => 'a.linker-create-btn').click - @driver.clear_and_send_keys([:id, 'digital_object_notes__0__label_'], "Summary label") + modal = @driver.find_element(:css => '#resource_instances__0__digital_object__ref__modal') + + modal.clear_and_send_keys([:id, "digital_object_title_"],("digital_object_title")) + modal.clear_and_send_keys([:id, "digital_object_digital_object_id_"],(Digest::MD5.hexdigest("#{Time.now}"))) + + @driver.execute_script("$('#digital_object_notes.initialised .subrecord-form-heading .btn.add-note').focus()") + modal.find_element(:css => '#digital_object_notes.initialised .subrecord-form-heading .btn.add-note').click + modal.find_last_element(:css => '#digital_object_notes select.top-level-note-type').select_option_with_text("Summary") + + modal.clear_and_send_keys([:id, 'digital_object_notes__0__label_'], "Summary label") @driver.execute_script("$('#digital_object_notes__0__content__0_').data('CodeMirror').setValue('Summary content')") @driver.execute_script("$('#digital_object_notes__0__content__0_').data('CodeMirror').save()") @driver.execute_script("$('#digital_object_notes__0__content__0_').data('CodeMirror').toTextArea()") @driver.find_element(:id => "digital_object_notes__0__content__0_").attribute("value").should eq("Summary content") - @driver.find_element(:id, "createAndLinkButton").click - @driver.wait_for_ajax - @driver.find_element(:css => "form#resource_form button[type='submit']").click - @driver.wait_for_ajax - + modal.find_element(:id, "createAndLinkButton").click + @driver.click_and_wait_until_gone(:css => "form#resource_form button[type='submit']") + @driver.find_element(:css, ".token-input-token .digital_object").click # so the subject is here now @@ -248,33 +252,28 @@ @driver.click_and_wait_until_gone(:id => "createPlusOne") end - elements = @driver.blocking_find_elements(:css => "li.jstree-leaf").map{|li| li.text.strip} + elements = tree_nodes_at_level(1).map{|li| li.text.strip} ["January", "February", "December"].each do |month| elements.any? {|elt| elt =~ /#{month}/}.should be_truthy end + + @driver.click_and_wait_until_gone(:css, "a.btn.btn-cancel") end it "can cancel edits to Archival Objects" do ao_id = @archival_object.uri.sub(/.*\//, '') @driver.get("#{$frontend}#{@resource.uri.sub(/\/repositories\/\d+/, '')}/edit#tree::archival_object_#{ao_id}") - + # sanity check.. - @driver.find_element(:id => js_node(@archival_object).a_id).click + tree_click(tree_node(@archival_object)) pane_resize_handle = @driver.find_element(:css => ".ui-resizable-handle.ui-resizable-s") - 10.times { - @driver.action.drag_and_drop_by(pane_resize_handle, 0, 30).perform - } @driver.clear_and_send_keys([:id, "archival_object_title_"], "unimportant change") - @driver.find_element(:id => js_node(@resource).a_id).click - sleep(5) - @driver.find_element(:id, "dismissChangesButton").click - assert(5) { - @driver.find_element(:css => "a.jstree-clicked .title-column").text.delete("1").chomp.strip.should eq(@resource.title.delete("1").chomp.strip) - } + tree_click(tree_node(@resource)) + @driver.click_and_wait_until_gone(:id, "dismissChangesButton") end @@ -282,29 +281,33 @@ ao_id = @archival_object.uri.sub(/.*\//, '') @driver.get("#{$frontend}#{@resource.uri.sub(/\/repositories\/\d+/, '')}/edit#tree::archival_object_#{ao_id}") + # Wait for the form to load in + @driver.find_element(:css => "form#archival_object_form button[type='submit']") + @driver.find_element(:id, "archival_object_level_").select_option("item") @driver.clear_and_send_keys([:id, "archival_object_title_"], "") - sleep(5) - @driver.find_element(:css => "form .record-pane button[type='submit']").click - @driver.wait_for_ajax + @driver.click_and_wait_until_gone(:css => "form .record-pane button[type='submit']") + expect { @driver.find_element_with_text('//div[contains(@class, "error")]', /Title - must not be an empty string/) }.to_not raise_error - - @driver.find_element(:link, "Revert Changes").click - @driver.find_element(:id, "dismissChangesButton").click + tree_click(tree_node(@resource)) + @driver.click_and_wait_until_gone(:id, "dismissChangesButton") end it "can update an existing Archival Object" do @driver.get_edit_page(@archival_object) + # Wait for the form to load in + @driver.find_element(:css => "form#archival_object_form button[type='submit']") + @driver.clear_and_send_keys([:id, "archival_object_title_"], "save this please") @driver.find_element(:css => "form .record-pane button[type='submit']").click @driver.wait_for_ajax assert(5) { @driver.find_element(:css, "h2").text.should eq("save this please Archival Object") } assert(5) { @driver.find_element(:css => "div.alert.alert-success").text.should eq('Archival Object save this please updated') } @driver.clear_and_send_keys([:id, "archival_object_title_"], @archival_object.title) - @driver.find_element(:css => "form .record-pane button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form .record-pane button[type='submit']") end @@ -341,7 +344,7 @@ # so the subject is here now assert(5) { @driver.find_element(:css, "#archival_object_subjects_ ul.token-input-list").text.should match(/#{$$}FooTerm456/) } end - + it "can view a read only Archival Object" do @@ -353,7 +356,7 @@ end - it "exports and downloads the resource to xml" do + xit "exports and downloads the resource to xml" do @driver.get_view_page(@resource) @driver.find_element(:link, "Export").click diff --git a/selenium/spec/search_spec.rb b/selenium/spec/search_spec.rb index a4f8722123..3d6160e292 100644 --- a/selenium/spec/search_spec.rb +++ b/selenium/spec/search_spec.rb @@ -3,7 +3,7 @@ describe "Search" do before(:all) do - @repo = create(:repo, :repo_code => "search_test_#{Time.now.to_i}") + @repo = create(:repo, :repo_code => "search_test_#{Time.now.to_i}", :publish => true) set_repo @repo @accession = create(:accession, @@ -55,7 +55,9 @@ describe "Advanced Search" do before(:all) do - @repo = create(:repo, :repo_code => "adv_search_test_#{Time.now.to_i}") + @repo = create(:repo, + :repo_code => "adv_search_test_#{Time.now.to_i}", + :publish => true) set_repo @repo @keywords = (0..9).to_a.map { SecureRandom.hex } @@ -93,12 +95,7 @@ it "is available via the navbar and renders when toggled" do @driver.find_element(:css => ".navbar .search-switcher").click - - assert(10) { - advanced_search_form = @driver.find_element(:css => "form.advanced-search") - advanced_search_form.find_element(:id => "v0") - advanced_search_form.find_element(:css => ".btn-primary") - } + @driver.find_element(:css => ".search-switcher-hide") end diff --git a/selenium/spec/space_calculator_spec.rb b/selenium/spec/space_calculator_spec.rb index f99868b9df..f38c472aa2 100644 --- a/selenium/spec/space_calculator_spec.rb +++ b/selenium/spec/space_calculator_spec.rb @@ -79,6 +79,7 @@ @driver.navigate.to("#{$frontend}/top_containers/#{@top_container.id}/edit") @driver.find_element(:css => "#top_container_container_locations_ .subrecord-form-heading .btn").click @driver.find_element(:css => "#top_container_container_locations_ .linker-wrapper .btn.locations").click + @driver.wait_for_dropdown @driver.find_element(:link => "Find with Space Calculator").click @driver.find_element(:id, "spaceCalculatorModal") @@ -99,10 +100,20 @@ end it "can select a location from the calculator results to populate the Container's Location field" do + # a row with space exists row = @driver.find_element(:css => "#tabledSearchResults tr.has-space") - row.find_element(:id => "linker-item__locations_#{@location.id}").click - @driver.find_element(:css => "#spaceCalculatorModal .modal-footer #addSelectedButton").click + # clicking the row will select the row + @driver.execute_script("$('#tabledSearchResults tr.has-space td:first').click()"); + + # the radio should be checked + expect(@driver.execute_script("return $('#linker-item__locations_#{@location.id}').is(':checked')")).to be_truthy + + # and the row selected + expect(row.attribute('class')).to include('selected') + + # the add button will now be enabled + @driver.find_element(:css => "#spaceCalculatorModal .modal-footer #addSelectedButton:not([disabled])").click @driver.find_element(:css, "#top_container_container_locations_ ul.token-input-list").text.should match(/#{Regexp.quote(@location.title)}/) @driver.find_element(:css => ".record-pane .form-actions .btn.btn-primary").click end diff --git a/selenium/spec/spec_helper.rb b/selenium/spec/spec_helper.rb index 7e7502c5be..1087159ca7 100644 --- a/selenium/spec/spec_helper.rb +++ b/selenium/spec/spec_helper.rb @@ -1,4 +1,5 @@ require_relative 'factories' +require 'pry' require_relative "../common" require_relative '../../indexer/app/lib/realtime_indexer' @@ -48,7 +49,7 @@ end config.include BackendClientMethods - config.include JSTreeHelperMethods + config.include TreeHelperMethods config.include FactoryGirl::Syntax::Methods config.extend RSpecClassHelpers config.verbose_retry = true @@ -60,7 +61,7 @@ # runs indexers in the same thread as the tests if necessary if !ENV['ASPACE_INDEXER_URL'] $indexer = RealtimeIndexer.new($backend, nil) - $period = PeriodicIndexer.new + $period = PeriodicIndexer.new($backend, nil, "periodic_indexer") end end diff --git a/selenium/spec/subjects_spec.rb b/selenium/spec/subjects_spec.rb index 7a47d6ad36..53af19c8ae 100644 --- a/selenium/spec/subjects_spec.rb +++ b/selenium/spec/subjects_spec.rb @@ -17,8 +17,12 @@ end it "reports errors and warnings when creating an invalid Subject" do + @driver.get($frontend) + @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Subject').click + @driver.click_and_wait_until_gone(:link => 'Subject') + + @driver.find_element(:css => '#subject_terms_.initialised') @driver.find_element(:css => '#subject_external_documents_ .subrecord-form-heading .btn:not(.show-all)').click @@ -37,38 +41,53 @@ it "can create a new Subject" do now = "#{$$}.#{Time.now.to_i}" + @driver.get($frontend) + @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Subject').click - @driver.find_element(:css => "form #subject_terms_ button:not(.show-all)").click + @driver.click_and_wait_until_gone(:link => 'Subject') - @driver.find_element(:id => "subject_source_").select_option("local") + @driver.find_element(:css => "#subject_terms_.initialised") + + @driver.find_element(:css => "form #subject_terms_.initialised button:not(.show-all)").click + @driver.find_element(:id => "subject_source_").select_option("local") @driver.clear_and_send_keys([:id, "subject_terms__0__term_"], "just a term really #{now}") @driver.clear_and_send_keys([:id, "subject_terms__1__term_"], "really") - @driver.find_element(:css => "form .record-pane button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form .record-pane button[type='submit']") assert(5) { @driver.find_element(:css => '.record-pane h2').text.should eq("just a term really #{now} -- really Subject") } end - it "can reorder the terms and have them maintain order" do + xit "can reorder the terms and have them maintain order" do + @driver.get($frontend) first = "first_#{SecureRandom.hex}" second = "second_#{SecureRandom.hex}" @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Subject').click + @driver.click_and_wait_until_gone(:link => 'Subject') + + @driver.find_element(:css => '#subject_terms_.initialised') + @driver.find_element(:css => "form #subject_terms_ button:not(.show-all)").click @driver.find_element(:id => "subject_source_").select_option("local") @driver.clear_and_send_keys([:id, "subject_terms__0__term_"], first) @driver.clear_and_send_keys([:id, "subject_terms__1__term_"], second) - @driver.find_element(:css => "form .record-pane button[type='submit']").click + @driver.click_and_wait_until_gone(:css => "form .record-pane button[type='submit']") assert(5) { @driver.find_element(:css => '.record-pane h2').text.should eq("#{first} -- #{second} Subject") } #drag to become sibling of parent source = @driver.find_element( :css => "#subject_terms__1_ .drag-handle" ) - @driver.action.drag_and_drop_by(source, 0, -100).perform - - # I hate you for wasting my life. + + # Tuesday 14 March 14:33:42 AEDT 2017 -- selenium rejecting the negative Y + # value here, which seems like a bug: + # + # https://github.com/mozilla/geckodriver/issues/527 + @driver.action.drag_and_drop_by(source, 0, -100).perform + + # I hate you for wasting my life. + # + # I concur. @driver.find_element( :id => "subject_terms_" ).click sleep(2) @driver.find_element(:css => "form .record-pane button[type='submit']").click @@ -84,10 +103,12 @@ end it "can present a browse list of Subjects" do + @driver.get($frontend) + run_index_round @driver.find_element(:link => 'Browse').click - @driver.find_element(:link => 'Subjects').click + @driver.click_and_wait_until_gone(:link => 'Subjects') expect { @driver.find_element_with_text('//tr', /just a term really/) @@ -96,10 +117,12 @@ it "can use plus+1 submit to quickly add another" do + @driver.get($frontend) + now = "#{$$}.#{Time.now.to_i}" @driver.find_element(:link => 'Create').click - @driver.find_element(:link => 'Subject').click + @driver.click_and_wait_until_gone(:link => 'Subject') @driver.clear_and_send_keys([:id, "subject_terms__0__term_"], "My First New Term #{now}") @driver.find_element(:id => "subject_source_").select_option("local") @@ -113,7 +136,7 @@ run_index_round @driver.find_element(:link => 'Browse').click - @driver.find_element(:link => 'Subjects').click + @driver.click_and_wait_until_gone(:link => 'Subjects') @driver.find_element(:link => "Download CSV").click sleep(1) diff --git a/selenium/spec/system_information_spec.rb b/selenium/spec/system_information_spec.rb index 15409763dd..d01048f0e4 100644 --- a/selenium/spec/system_information_spec.rb +++ b/selenium/spec/system_information_spec.rb @@ -27,7 +27,7 @@ end - it "should let the admin see this", :retry => 2, :retry_wait => 10 do + it "should let the admin see this" do @driver.login_to_repo($admin, @repo) @driver.find_element(:link, "System").click diff --git a/selenium/spec/trees_spec.rb b/selenium/spec/trees_spec.rb index 1eea0ea9bb..4d2b6b9073 100644 --- a/selenium/spec/trees_spec.rb +++ b/selenium/spec/trees_spec.rb @@ -31,175 +31,127 @@ it "can add a sibling" do - @driver.find_elements(:css => "li.jstree-node").length.should eq(4) + @driver.find_elements(:css => ".root-row").length.should eq(1) + @driver.find_elements(:css => ".largetree-node").length.should eq(3) - @driver.find_element(:id, js_node(@a3).a_id).click - @driver.wait_for_ajax + tree_click(tree_node(@a3)) + + tree_add_sibling - @driver.click_and_wait_until_gone(:link, "Add Sibling") @driver.clear_and_send_keys([:id, "archival_object_title_"], "Sibling") @driver.find_element(:id, "archival_object_level_").select_option("item") @driver.click_and_wait_until_gone(:css, "form#archival_object_form button[type='submit']") - @driver.wait_for_ajax - @driver.find_elements(:css => "li.jstree-node").length.should eq(5) + @driver.find_elements(:css => ".largetree-node").length.should eq(4) # reload the parent form to make sure the changes stuck @driver.get("#{$frontend}/#{@r.uri.sub(/\/repositories\/\d+/, '')}/edit") @driver.wait_for_ajax - @driver.find_elements(:css => "li.jstree-node").length.should eq(5) + @driver.find_elements(:css => ".root-row").length.should eq(1) + @driver.find_elements(:css => ".largetree-node").length.should eq(4) end it "can support dnd: into a sibling archival object" do @driver.navigate.refresh - # first resize the tree pane (do it incrementally so it doesn't flip out...) - pane_resize_handle = @driver.find_element(:css => ".ui-resizable-handle.ui-resizable-s") - 10.times { - @driver.action.drag_and_drop_by(pane_resize_handle, 0, 30).perform - } - source = @driver.find_element(:id, js_node(@a1).a_id) - target = @driver.find_element(:id, js_node(@a2).a_id) + tree_enable_reorder_mode - y_off = target.location[:y] - source.location[:y] + source = @driver.find_element(:id, tree_node(@a1).tree_id) + target = @driver.find_element(:id, tree_node(@a2).tree_id) - @driver.action.drag_and_drop_by(source, 0, y_off).perform - @driver.wait_for_ajax - @driver.wait_for_spinner - - - target = @driver.find_element(:id, js_node(@a2).li_id) - # now open the target - 3.times do - begin - target.find_element(:css => "i.jstree-icon").click - target.find_element_orig(:css => "ul.jstree-children") - break - rescue - $stderr.puts "hmm...lets try and reopen the node" - sleep(2) - next - end - end - # and find the former sibling - target.find_element(:id, js_node(@a1).li_id) + tree_drag_and_drop(source, target, 'Add Items as Children') + + # and find the former sibling and check indent + source = @driver.find_element(:id, tree_node(@a1).tree_id) + expect(source.attribute('class')).to include('indent-level-2') # refresh the page and verify that the change really stuck @driver.navigate.refresh - # same check again - @driver.find_element(:id, js_node(@a2).li_id) - .find_element(:css => "i.jstree-icon").click + @driver.find_element(:id, tree_node(@a2).tree_id) + .find_element(:css => ".expandme").click # but this time wait for lazy loading and re-find the parent node @driver.wait_for_ajax - target = @driver.find_element(:id, js_node(@a2).li_id) - target.find_element(:id, js_node(@a1).li_id) + + target = @driver.find_element(:id, tree_node(@a2).tree_id) + expect(target.attribute('class')).to include('indent-level-1') + + source = @driver.find_element(:id, tree_node(@a1).tree_id) + expect(source.attribute('class')).to include('indent-level-2') end - # TODO: review this test when things quiet down? it "can not reorder the tree while editing a node" do - pane_resize_handle = @driver.find_element(:css => ".ui-resizable-handle.ui-resizable-s") - 10.times { - @driver.action.drag_and_drop_by(pane_resize_handle, 0, 30).perform - } - - @driver.find_element(:id, js_node(@a3).a_id).click - @driver.wait_for_ajax + tree_click(tree_node(@a3)) @driver.clear_and_send_keys([:id, "archival_object_title_"], @a3.title.sub(/Archival Object/, "Resource Component")) - moving = @driver.find_element(:id, js_node(@a3).li_id) - target = @driver.find_element(:id, js_node(@a2).li_id) + expect(tree_enable_reorder_toggle.attribute('class')).to include('disabled') - # now do a drag and drop - @driver.action.drag_and_drop(moving, target).perform - @driver.wait_for_ajax + @driver.ensure_no_such_element(:css, '.largetree-node .drag-handle') # save the item @driver.click_and_wait_until_gone(:css => "form#archival_object_form button[type='submit']") - @driver.wait_for_ajax - - # open the node (maybe this should happen by default?) - @driver.find_element(:id => js_node(@a2).li_id) - .find_element(:css => "i.jstree-icon").click - - sleep(5) - - # we expect the move to have been rebuffed - expect { - @driver - .find_element(:id => js_node(@a2).li_id) - .find_element_orig(:id => js_node(@a3).li_id) - }.to raise_error Selenium::WebDriver::Error::NoSuchElementError - - # if we refresh the parent should now be open - @driver.navigate.refresh - - @driver - .find_element(:id => js_node(@a3).li_id).find_element(:css => "span.title-column").text.should match(/Resource Component/) - end - it "can move tree nodes into and out of each other", :retry => 2, :retry_wait => 10 do + it "can move tree nodes into and out of each other" do + tree_enable_reorder_mode - # move siblings 2 and 3 into 1 [@a2, @a3].each do |sibling| - @driver.find_element(:id => js_node(sibling).a_id).click - @driver.wait_for_ajax + tree_click(tree_node(sibling)) @driver.find_element(:link, "Move").click - - el = @driver.find_element_with_text("//a", /Down Into/) - @driver.mouse.move_to el - @driver.wait_for_ajax + @driver.execute_script('$("#tree-toolbar .dropdown-submenu:visible").addClass("open")') - @driver.find_element(:css => "div.move-node-menu") - .find_element(:xpath => ".//a[@data-target-node-id='#{js_node(@a1).li_id}']") + @driver.find_element(:css => "ul.move-node-into-menu") + .find_element(:xpath => ".//a[@data-tree_id='#{tree_node(@a1).tree_id}']") .click - @driver.wait_for_ajax - @driver.wait_for_spinner - sleep(2) + @driver.execute_script('$("#tree-toolbar .dropdown-submenu:visible").removeClass("open")') + + tree_wait_for_spinner end + tree_disable_reorder_mode + 2.times {|i| - @driver.find_element(:id => js_node(@a1).a_id).click - @driver.wait_for_ajax - # @driver.wait_for_spinner - [@a2, @a3].each do |child| - @driver.find_element(:id => js_node(@a1).li_id) - .find_element(:id => js_node(child).li_id) + tree_click(tree_node(@a1)) + + [@a2, @a3].each do |child_ao| + parent = @driver.find_element(:id => tree_node(@a1).tree_id) + expect(parent.attribute('class')).to include('indent-level-1') + + child = @driver.find_element(:id => tree_node(child_ao).tree_id) + expect(child.attribute('class')).to include('indent-level-2') end @driver.navigate.refresh if i == 0 } # now move them back + tree_enable_reorder_mode + [@a2, @a3].each do |child| - @driver.find_element(:id => js_node(child).a_id).click - @driver.wait_for_ajax + tree_click(tree_node(child)) @driver.find_element(:link, "Move").click @driver.find_element_with_text("//a", /Up a Level/).click - @driver.wait_for_ajax - @driver.wait_for_spinner - - sleep(2) + tree_wait_for_spinner end + tree_disable_reorder_mode 2.times {|i| [@a2, @a3].each do |sibling| - @driver.find_element(:id => js_node(sibling).li_id) - .find_element(:xpath => "following-sibling::li[@id='#{js_node(@a1).li_id}']") + node = @driver.find_element(:id => tree_node(sibling).tree_id) + expect(node.attribute('class')).to include('indent-level-1') end @driver.navigate.refresh if i == 0 @@ -209,13 +161,13 @@ it "can delete a node and return to its parent" do - @driver.find_element(:id => js_node(@a1).a_id).click - @driver.wait_for_ajax + tree_click(tree_node(@a1)) @driver.find_element(:css, ".delete-record.btn").click - @driver.find_element(:css, "#confirmChangesModal #confirmButton").click + @driver.click_and_wait_until_gone(:css, "#confirmChangesModal #confirmButton") - @driver.find_element(:id => js_node(@r).li_id).attribute("class").split(" ").should include('primary-selected') + node = @driver.find_element(:id => tree_node(@r).tree_id) + expect(node.attribute('class')).to include('current') end @@ -223,334 +175,9 @@ @driver.login_to_repo(@viewer_user, @repo) @driver.get_view_page(@r) - pane_resize_handle = @driver.find_element(:css => ".ui-resizable-handle.ui-resizable-s") - 10.times { - @driver.action.drag_and_drop_by(pane_resize_handle, 0, 30).perform - } - - moving = @driver.find_element(:id => js_node(@a1).li_id) - target = @driver.find_element(:id => js_node(@a2).li_id) - - # now do a drag and drop - @driver.action.drag_and_drop(moving, target).perform - - moving.find_elements(:xpath => "following-sibling::li").length.should eq(2) + @driver.ensure_no_such_element(:link, 'Enable Reorder Mode') + @driver.ensure_no_such_element(:css, '.largetree-node .drag-handle') @driver.login_to_repo($admin, @repo) end - it "can celebrate the birth of jesus christ our lord" do - @driver.click_and_wait_until_gone(:link, "Add Child") - @driver.clear_and_send_keys([:id, "archival_object_title_"], "Gifts") - @driver.find_element(:id, "archival_object_level_").select_option("collection") - @driver.click_and_wait_until_gone(:css, "form#archival_object_form button[type='submit']") - - # lets add some nodes - first_born = nil - ["Fruit Cake", "Ham", "Coca-cola bears"].each_with_index do |ao, i| - - unless first_born - # lets make a baby! - @driver.click_and_wait_until_gone(:link, "Add Child") - first_born = ao - else - # really?!? another one?!? - @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{first_born}/).click - @driver.click_and_wait_until_gone(:link, "Add Sibling") - end - - @driver.clear_and_send_keys([:id, "archival_object_title_"], ao) - @driver.find_element(:id, "archival_object_level_").select_option("item") - @driver.click_and_wait_until_gone(:css, "form#archival_object_form button[type='submit']") - end - - # open the tree a little - dragger = @driver.find_element(:css => ".ui-resizable-handle.ui-resizable-s" ) - @driver.action.drag_and_drop_by(dragger, 0, 300).perform - @driver.wait_for_ajax - - - # now lets move and delete some nodes - ["Ham", "Coca-cola bears"].each do |ao| - target = @driver.find_element_with_text("//div[@id='archives_tree']//a", /Gifts/) - source = @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{ao}/) - y_off = target.location[:y] - source.location[:y] - - @driver.action.drag_and_drop_by(source, 0, y_off - 10).perform - @driver.wait_for_spinner - @driver.wait_for_ajax - - @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{ao}/).click - @driver.find_element(:link, "Move").click - @driver.find_element(:link, "Up").click - @driver.wait_for_spinner - @driver.wait_for_ajax - - @driver.find_element_with_text("//div", /Please click to load records/).click - - @driver.find_element(:css, ".alert-info").click - @driver.wait_for_ajax - - @driver.find_element_with_text("//div[@id='archives_tree']//a", /Gifts/).click - @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{ao}/).click - @driver.wait_for_ajax - - @driver.find_element(:css, ".delete-record.btn").click - @driver.wait_for_ajax - sleep(2) - - @driver.find_element(:css, "#confirmChangesModal #confirmButton").click - @driver.click_and_wait_until_gone(:link, "Edit") - @driver.click_and_wait_until_gone(:css, "li.jstree-closed > i.jstree-icon") - end - - @driver.find_element(:id, js_node(@r).li_id).click - @driver.find_element_with_text("//div[@id='archives_tree']//a", /Gifts/).click - @driver.click_and_wait_until_gone(:link, "Add Sibling") - @driver.clear_and_send_keys([:id, "archival_object_title_"], "Nothing") - @driver.find_element(:id, "archival_object_level_").select_option("item") - @driver.click_and_wait_until_gone(:css, "form#archival_object_form button[type='submit']") - - - # now lets add some more and move them around - [ "Santa Crap", "Japanese KFC", "Kalle Anka"].each do |ao| - @driver.wait_for_ajax - @driver.find_element(:id, js_node(@r).li_id).click - - @driver.wait_for_ajax - @driver.find_element_with_text("//div[@id='archives_tree']//a", /Gifts/).click - @driver.wait_for_ajax - sleep(2) - - - @driver.click_and_wait_until_gone(:link, "Add Sibling") - @driver.clear_and_send_keys([:id, "archival_object_title_"], ao) - @driver.find_element(:id, "archival_object_level_").select_option("item") - @driver.click_and_wait_until_gone(:css, "form#archival_object_form button[type='submit']") - - target = @driver.find_element_with_text("//div[@id='archives_tree']//a", /Gifts/) - source = @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{ao}/) - - y_off = target.location[:y] - source.location[:y] - @driver.action.drag_and_drop_by(source, 0, y_off).perform - @driver.wait_for_ajax - @driver.wait_for_spinner - end - - wait = Selenium::WebDriver::Wait.new(:timeout => 40) - @driver.find_element(:id, js_node(@r).li_id).click - @driver.click_and_wait_until_gone(:link, 'Close Record') - @driver.wait_for_ajax - sleep(2) - - - # now lets add some notes - [ "Japanese KFC", "Kalle Anka", "Santa Crap"].each do |ao| - - # sanity check to make sure we're editing.. - edit_btn = @driver.find_element_with_text("//div[@class='record-toolbar']/div/a", /Edit/, true, true) - - if edit_btn - @driver.click_and_wait_until_gone(:link, 'Edit') - end - - @driver.find_element(:id, js_node(@r).li_id).click - - @driver.find_element_with_text("//div[@id='archives_tree']//a", /Gifts/).click - @driver.find_element(:css, "a.refresh-tree").click - @driver.wait_for_ajax - - @driver.find_element_with_text("//div[@id='archives_tree']//a", /#{ao}/).click - @driver.wait_for_ajax - @driver.find_element_with_text("//button", /Add Note/).click - # @driver.find_element(:css => '#notes .subrecord-form-heading .btn:not(.show-all)').click - @driver.find_last_element(:css => '#notes select.top-level-note-type:last-of-type').select_option("note_multipart") - @driver.clear_and_send_keys([:id, 'archival_object_notes__0__label_'], "A multipart note") - @driver.execute_script("$('#archival_object_notes__0__subnotes__0__content_').data('CodeMirror').setValue('Some note content')") - @driver.execute_script("$('#archival_object_notes__0__subnotes__0__content_').data('CodeMirror').save()") - @driver.click_and_wait_until_gone(:css => "form#archival_object_form button[type='submit']") - @driver.find_element(:link, 'Close Record').click - end - - # everything should be in the order we want it... - [ "Kalle Anka", "Japanese KFC","Santa Crap", "Fruit Cake" ].each_with_index do |ao, i| - ao.delete!("1") - assert(5) { - @driver.find_element( :xpath => "//div[@id='archives_tree']//li[a/@title='Gifts']/ul/li[position() = #{i + 1}]/a/span/span[@class='title-column pull-left']").text.should match(/#{ao}/) - } - end - end - - it "can make and mess with bigger trees" do - - # let's make some children - children = [] - 9.times { |i| children << create(:archival_object, :title => i.to_s, :resource => {:ref => @r.uri}, :parent => {:ref => @a1.uri}).uri } - @driver.navigate.refresh - - # go to the page.. - @driver.find_element( :xpath => "//a[@title='#{@a1.title}']").click - @driver.wait_for_ajax - - # lets make the pane nice and big... - last = @driver.find_elements(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li/a").last - first = @driver.find_elements(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li/a").first - pane_size = last.location[:y] - first.location[:y] - pane_resize_handle = @driver.find_element(:css => ".ui-resizable-handle.ui-resizable-s") - @driver.action.drag_and_drop_by(pane_resize_handle, 0, ( pane_size * 2 ) ).perform - - # we cycle these nodes around in a circle. - 3.times do - a = @driver.element_finder(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li[7]/a") - b = @driver.element_finder(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li[9]/a") - target = @driver.find_elements( - :xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li").first - offset = ( ( target.location[:y] - a.call.location[:y] ) - 9 ) - a.call.click - @driver.action.key_down(:shift).perform - b.call.click - @driver.action.key_up(:shift).perform - @driver.action.drag_and_drop_by(a.call, 0, offset).perform - - @driver.wait_for_ajax - @driver.wait_for_spinner - end - - # everything should be normal - (0..8).each do |i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@a1.title}']/ul/li[position() = #{i + 1}]/a/span/span[@class='title-column pull-left']").text.should match(/#{i.to_s}/) - } - end - - # now lets cycles in reverse - 3.times do - a = @driver.element_finder(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li[1]/a") - b = @driver.element_finder(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li[3]/a") - target = @driver.find_elements( - :xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li").last - offset = ( ( target.location[:y] - a.call.location[:y] ) + 7 ) - # @driver.action.click(a).key_down(:shift).click(b).key_up(:shift).drag_and_drop_by(a, 0, offset).perform - a.call.click - @driver.action.key_down(:shift).perform - b.call.click - @driver.action.key_up(:shift).perform - @driver.action.drag_and_drop_by(a.call, 0, offset).perform - - @driver.wait_for_ajax - @driver.wait_for_spinner - end - - # back to normal - (0..8).each do |i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@a1.title}']/ul/li[position() = #{i + 1 }]/a/span/span[@class='title-column pull-left']").text.should match(/#{i.to_s}/) - } - end - - # now lets stick some in the middle - 2.times do - a = @driver.element_finder(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li[1]/a") - b = @driver.element_finder(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li[3]/a") - target = @driver.find_elements( - :xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li")[5] - offset = ( ( target.location[:y] - a.call.location[:y] ) + 7 ) - a.call.click - @driver.action.key_down(:shift).perform - b.call.click - @driver.action.key_up(:shift).perform - @driver.action.drag_and_drop_by(a.call, 0, offset).perform - - @driver.wait_for_ajax - @driver.wait_for_spinner - end - - # and again back to normal - (0..8).each do |i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@a1.title}']/ul/li[position() = #{i + 1}]/a/span/span[@class='title-column pull-left']").text.should match(/#{i.to_s}/) - } - end - - # and now let's move them up a level and do it all again... - a = @driver.find_elements(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li/a").first - b = @driver.find_elements(:xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']/ul/li/a").last - target = @driver.find_element( - :xpath => "//div[@id='archives_tree']//li[a/@title='#{@a1.title}']") - - offset = ( ( target.location[:y] - a.location[:y] ) - 9 ) - @driver.action.click(a).key_down(:shift).click(b).key_up(:shift).drag_and_drop_by(a, 0, offset).perform #fails here - @driver.wait_for_ajax - @driver.wait_for_spinner - - # heres the new order of our AOs. all on one level - new_order = (0..8).to_a + [ @a1.title, @a2.title, @a3.title ] - - # let's check that everything is as expected - new_order.each_with_index do |v, i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@r.title}']/ul/li[position() = #{i + 1 }]/a/span/span[@class='title-column pull-left']").text.should match(/#{v}/) - } - end - - # let's cycle bottom to top - 4.times do - new_order = new_order.pop(3) + new_order - a = @driver.find_elements(:xpath => "//li[a/@title='#{@r.title}']/ul/li/a")[-3] - b = @driver.find_elements(:xpath => "//li[a/@title='#{@r.title}']/ul/li/a").last - target = @driver.find_elements( - :xpath => "//li[a/@title='#{@r.title}']/ul/li").first - offset = ( ( target.location[:y] - a.location[:y] ) - 9 ) - @driver.action.click(a).key_down(:shift).click(b).key_up(:shift).drag_and_drop_by(a, 0, offset).perform - @driver.wait_for_ajax - @driver.wait_for_spinner - new_order.each_with_index do |v, i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@r.title}']/ul/li[position() = #{i + 1 }]/a/span/span[@class='title-column pull-left']").text.should match(/#{v}/) - } - end - end - - # let's cycle top to bottom - 4.times do - n = new_order.shift(3) - new_order = new_order + n - a = @driver.find_elements(:xpath => "//li[a/@title='#{@r.title}']/ul/li/a").first - b = @driver.find_elements(:xpath => "//li[a/@title='#{@r.title}']/ul/li/a")[2] - target = @driver.find_elements( - :xpath => "//li[a/@title='#{@r.title}']/ul/li").last - offset = ( ( target.location[:y] - a.location[:y] ) + 9 ) - @driver.action.click(a).key_down(:shift).click(b).key_up(:shift).drag_and_drop_by(a, 0, offset).perform - @driver.wait_for_ajax - @driver.wait_for_spinner - new_order.each_with_index do |v, i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@r.title}']/ul/li[position() = #{i + 1 }]/a/span/span[@class='title-column pull-left']").text.should match(/#{v}/) - } - end - end - - # let's move top 3 into the middle and see if that works - 2.times do - n = new_order.shift(3) - new_order.insert(3, n).flatten! - - a = @driver.find_elements(:xpath => "//li[a/@title='#{@r.title}']/ul/li/a").first - b = @driver.find_elements(:xpath => "//li[a/@title='#{@r.title}']/ul/li/a")[2] - target = @driver.find_elements( - :xpath => "//li[a/@title='#{@r.title}']/ul/li")[5] - offset = ( ( target.location[:y] - a.location[:y] ) + 7 ) - @driver.action.click(a).key_down(:shift).click(b).key_up(:shift).drag_and_drop_by(a, 0, offset).perform - @driver.wait_for_ajax - @driver.wait_for_spinner - new_order.each_with_index do |v, i| - assert(5) { - @driver.find_element( :xpath => "//li[a/@title='#{@r.title}']/ul/li[position() = #{i + 1 }]/a/span/span[@class='title-column pull-left']").text.should match(/#{v}/) - } - end - end - - - end - - end diff --git a/selenium/spec/user_management_spec.rb b/selenium/spec/user_management_spec.rb index e34feef0d8..09fdf91a14 100644 --- a/selenium/spec/user_management_spec.rb +++ b/selenium/spec/user_management_spec.rb @@ -61,7 +61,7 @@ @driver.logout.login($admin) @driver.find_element(:link, 'System').click - @driver.find_element(:link, "Manage Users").click + @driver.click_and_wait_until_gone(:link, "Manage Users") @driver.find_paginated_element(:xpath => "//td[contains(text(), '#{@test_user.username}')]/following-sibling::td/div/a").click diff --git a/selenium/spec/user_preferences_spec.rb b/selenium/spec/user_preferences_spec.rb index 60ca58250b..c0c7c53a5a 100644 --- a/selenium/spec/user_preferences_spec.rb +++ b/selenium/spec/user_preferences_spec.rb @@ -21,17 +21,16 @@ it "allows you to configure browse columns" do - 2.times { - @driver.find_element(:css, '.user-container .btn.dropdown-toggle.last').click - @driver.find_element(:link, "My Repository Preferences").click + @driver.find_element(:css, '.user-container .btn.dropdown-toggle.last').click + @driver.wait_for_dropdown + @driver.find_element(:link, "My Repository Preferences").click - @driver.find_element(:id => "preference_defaults__accession_browse_column_1_").select_option_with_text("Acquisition Type") - @driver.find_element(:css => 'button[type="submit"]').click - @driver.find_element(:css => ".alert-success") - } + @driver.find_element(:id => "preference_defaults__accession_browse_column_1_").select_option_with_text("Acquisition Type") + @driver.click_and_wait_until_gone(:css => 'button[type="submit"]') + @driver.find_element(:css => ".alert-success") @driver.find_element(:link => 'Browse').click - @driver.find_element(:link => 'Accessions').click + @driver.click_and_wait_until_gone(:link => 'Accessions') @driver.find_element(:link => "Create Accession") cells = @driver.find_elements(:css, "table th") diff --git a/selenium/spec/users_and_auth_spec.rb b/selenium/spec/users_and_auth_spec.rb index 32034872a4..9cb7059182 100644 --- a/selenium/spec/users_and_auth_spec.rb +++ b/selenium/spec/users_and_auth_spec.rb @@ -14,10 +14,11 @@ it "fails logins with invalid credentials" do - @driver.login(OpenStruct.new(:username => "oopsie", - :password => "daisy")) + @driver.login(OpenStruct.new(:username => "oopsie", + :password => "daisy"), + expect_fail = true) - assert(5) { @driver.find_element(:css => "p.alert-danger").text.should eq('Login attempt failed') } + @driver.find_element(:css => "p.alert-danger").text.should eq('Login attempt failed') @driver.find_element(:link, "Sign In").click end diff --git a/solr/schema.xml b/solr/schema.xml index e182819032..40b3839861 100644 --- a/solr/schema.xml +++ b/solr/schema.xml @@ -47,6 +47,7 @@ + @@ -57,6 +58,7 @@ + @@ -64,7 +66,10 @@ + + + @@ -149,6 +154,10 @@ + + + + @@ -166,13 +175,14 @@ + - + @@ -297,12 +307,14 @@ --> + + @@ -316,7 +328,6 @@ -