Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b2537a2
Add rubocop binstub
kspurgin May 14, 2025
7377a06
Add VocabularyTerms::Handler.add_term opt_fields param
kspurgin May 14, 2025
20eef3b
Test ampersand escaping in full record mapping
kspurgin May 15, 2025
4163750
Extract #defuse_bomb to CollectionSpace::Mapper main module
kspurgin May 15, 2025
765dc40
Refactor VocabularyTerms::Handler methods
kspurgin May 15, 2025
e12501b
Defuse bombs and escape text when building VocabularyTerms payload
kspurgin May 15, 2025
4223bc1
Make PayloadBuilder take term_data param instead of termid
kspurgin May 15, 2025
8c100e0
WIP Implement VocabularyTerms::Hander.update_term
kspurgin May 15, 2025
401c89f
Set up VocabularyTerms::Handler to use Searcher
kspurgin May 15, 2025
49aaee0
Reorder Searcher methods
kspurgin May 15, 2025
6ce2d03
Refactor Searcher.response_item_count
kspurgin May 15, 2025
1710402
Require date in Error module
kspurgin May 15, 2025
a8d7bb0
feat: Implement updating vocab terms
kspurgin Jul 25, 2025
0acacc7
test: Update cassettes
kspurgin Jul 25, 2025
592b8b9
style: rubocop autocorrects
kspurgin Jul 25, 2025
cc60c49
feat: Add mode param to VocabularyTerms::PayloadBuilder.call
kspurgin Jul 25, 2025
762e789
feat: Validate opt_fields before processing VocabularyTerms::PayloadB…
kspurgin Jul 25, 2025
c42b6bf
refactor: Default to empty hash opt_fields value in PayloadBuilder
kspurgin Jul 26, 2025
ad6e5bd
refactor: Move changing of existing displayName to PayloadBuilder
kspurgin Jul 26, 2025
7c54078
test: Update cassettes
kspurgin Jul 26, 2025
735afd1
feat: Add delete_term method to vocabulary terms handler
kspurgin Aug 19, 2025
302b460
docs: Update changelog and document update and delete term usage
kspurgin Aug 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ This project bumps the version number for any changes (including documentation u
- none

## [Unreleased] - i.e. pushed to main branch but not yet tagged as a release
- Add rubocop binstub
- `CollectionSpace::Mapper::VocabularyTerms::Handler.add_term` now accepts an optional `opt_fields` Hash, allowing `description`, `source`, `sourcePage`, and `termStatus` values to be added with a term.
- `CollectionSpace::Mapper::VocabularyTerms::Handler` now has an `update_term` method which can be used to update the displayName of an existing term, or add additional fields to the term.
- `CollectionSpace::Mapper::VocabularyTerms::Handler` now has a `delete_term`

## [6.1.2] - 2025-08-13
- BUGFIX: the `methods` element was not getting added to the document structure for the exit procedure because it was getting interpreted as `Nokogiri::XML::Builder.methods`. This prevented the `method` field from being ingested. All keys are now sent to Builder with an underscore appended to prevent this behavior.
Expand Down
30 changes: 30 additions & 0 deletions bin/rubocop
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'rubocop' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)

bundle_binstub = File.expand_path("bundle", __dir__)

if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300).include?(
"This file was generated by Bundler"
)
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub "\
"cannot run. Replace `bin/bundle` by running `bundle binstubs "\
"bundler --force`, then run this command again.")
end
end

require "rubygems"
require "bundler/setup"

load Gem.bin_path("rubocop", "rubocop")
55 changes: 55 additions & 0 deletions doc/usage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -308,3 +308,58 @@ vh.add_term(vocab: 'annotationType', term: 'Credit line')
<2> When response.status_code = 409. For other non-success responses, the response itself will be returned in the Failure.

Calling `add_term` returns a https://dry-rb.org/gems/dry-monads/main/result/[dry-monads Result Success or Failure].

==== Populate other vocabulary item fields

Each vocabulary item has the following fields, in addition to its `displayName`:

* `description`
* `source`
* `sourcePage`
* `termStatus` (can be set to "active" or "inactive" by default)

To populate these, you can send a Hash representing the fields in as an `opt_fields` argument:

[source,ruby]
----
vh = CollectionSpace::Mapper::VocabularyTerms::Handler.new(client: client)
opts = {"description" => "Use when...", "termStatus" => "active"}
vh.add_term(vocab: 'Annotation Type', term: 'New term', opt_fields: opts)
----

=== Update vocabulary terms

==== Add additional fields to existing term

[source,ruby]
----
vh = CollectionSpace::Mapper::VocabularyTerms::Handler.new(client: client)
opts = {"description" => "Use to record object-level credit line. Is NOT published "\
"to public browser"}
vh.update_term(vocab: 'Annotation Type', term: 'Credit line', opt_fields: opts)
----

==== Updating display name of existing term

WARNING: Changing the display name of a term that is used in many fields may take a long time to update, since it needs to also update all usages of the term. It may even time out. We have not battle-tested this functionality in large sites, and it may not be suitable for batch-update use in some sites due to performance issues.

The `term` argument is the current term `displayName` value. This is used to look up and update the correct term record.

The new `displayName` value is passed in via `opt_fields`:

[source,ruby]
----
vh = CollectionSpace::Mapper::VocabularyTerms::Handler.new(client: client)
opts = {"displayName" => "Unpublished credit line"}
vh.update_term(vocab: 'Annotation Type', term: 'Credit line', opt_fields: opts)
----

=== Delete vocabulary terms

The usual restrictions on deletions apply here. You will not be able to delete a term that is in use.

[source,ruby]
----
vh = CollectionSpace::Mapper::VocabularyTerms::Handler.new(client: client)
vh.delete_term(vocab: 'Annotation Type', term: 'Unwanted term')
----
8 changes: 8 additions & 0 deletions lib/collectionspace/mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,13 @@ module Mapper
dateLatestQualifierUnit dateEarliestScalarValue
dateLatestScalarValue scalarValuesComputed],
reader: true

# @param xml [Nokogiri::XML::Document, String]
def defuse_bomb(xml)
xml.traverse do |node|
node.content = "" if node.text == CollectionSpace::Mapper.bomb
end
xml
end
end
end
8 changes: 1 addition & 7 deletions lib/collectionspace/mapper/data_mapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def map(xpath)
def clean_doc
remove_blank_nodes
handle_null_value_strings
defuse_bomb
CollectionSpace::Mapper.defuse_bomb(doc)
end

def remove_blank_nodes
Expand All @@ -107,12 +107,6 @@ def handle_null_value_strings
end
end

def defuse_bomb
doc.traverse do |node|
node.content = "" if node.text == CollectionSpace::Mapper.bomb
end
end

def add_namespaces
doc.xpath("/*/*").each do |section|
fetchuri = handler.record.ns_uri[section.name]
Expand Down
2 changes: 2 additions & 0 deletions lib/collectionspace/mapper/error.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "date"

module CollectionSpace
module Mapper
# Mixin module included in any application-specific error classes
Expand Down
79 changes: 37 additions & 42 deletions lib/collectionspace/mapper/searcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ def call(value:, type:, subtype: nil)

attr_reader :client, :active, :search_fields

def search_response(value, type, subtype)
as_is = get_response(value, type, subtype)
return as_is if response_usable?(as_is)

case_insensitive_response(value, type, subtype)
end

def get_response(value, type, subtype, operator = "=")
response = client.find(
type: type,
Expand All @@ -36,6 +43,35 @@ def get_response(value, type, subtype, operator = "=")
parse_response(response)
end

def response_usable?(response)
ct = response_item_count(response)
return false unless ct
return false if ct == 0

true
end

def case_insensitive_response(value, type, subtype)
response = get_response(value, type, subtype, "ILIKE")
return nil unless response_usable?(response)

displayname = response.dig("list_item", 0, "displayName") ||
response.dig("list_item", 0, "termDisplayName")
warning = {
category: "case_insensitive_match",
message: "Searched: #{value}. Using: #{displayname}"
}
response["warnings"] = [warning]
response
end

def search_field(type)
cached_field = search_fields[type]
return cached_field if cached_field

lookup_search_field(type)
end

def lookup_search_field(type)
field = CollectionSpace::Service.get(type: type)[:term]
rescue => e
Expand All @@ -58,48 +94,7 @@ def parse_response(response)
parsed
end

def response_item_count(response)
ct = response.dig("totalItems")
return ct.to_i if ct

nil
end

def response_usable?(response)
ct = response_item_count(response)
return false unless ct
return false if ct == 0

true
end

def search_field(type)
cached_field = search_fields[type]
return cached_field if cached_field

lookup_search_field(type)
end

def search_response(value, type, subtype)
as_is = get_response(value, type, subtype)
return as_is if response_usable?(as_is)

case_insensitive_response(value, type, subtype)
end

def case_insensitive_response(value, type, subtype)
response = get_response(value, type, subtype, "ILIKE")
return nil unless response_usable?(response)

displayname = response.dig("list_item", 0, "displayName") ||
response.dig("list_item", 0, "termDisplayName")
warning = {
category: "case_insensitive_match",
message: "Searched: #{value}. Using: #{displayname}"
}
response["warnings"] = [warning]
response
end
def response_item_count(response) = response.dig("totalItems")&.to_i
end
end
end
Loading