dor-services 6.0.5 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dor-services.rb +27 -12
  3. data/lib/dor/config.rb +45 -42
  4. data/lib/dor/datastreams/administrative_metadata_ds.rb +137 -44
  5. data/lib/dor/datastreams/content_metadata_ds.rb +42 -42
  6. data/lib/dor/datastreams/datastream_spec_solrizer.rb +1 -1
  7. data/lib/dor/datastreams/default_object_rights_ds.rb +185 -44
  8. data/lib/dor/datastreams/desc_metadata_ds.rb +36 -28
  9. data/lib/dor/datastreams/embargo_metadata_ds.rb +12 -14
  10. data/lib/dor/datastreams/events_ds.rb +10 -10
  11. data/lib/dor/datastreams/geo_metadata_ds.rb +4 -5
  12. data/lib/dor/datastreams/identity_metadata_ds.rb +14 -14
  13. data/lib/dor/datastreams/rights_metadata_ds.rb +23 -23
  14. data/lib/dor/datastreams/role_metadata_ds.rb +61 -15
  15. data/lib/dor/datastreams/simple_dublin_core_ds.rb +8 -8
  16. data/lib/dor/datastreams/version_metadata_ds.rb +10 -12
  17. data/lib/dor/datastreams/workflow_definition_ds.rb +6 -6
  18. data/lib/dor/datastreams/workflow_ds.rb +13 -13
  19. data/lib/dor/exceptions.rb +2 -2
  20. data/lib/dor/indexers/data_indexer.rb +1 -7
  21. data/lib/dor/indexers/describable_indexer.rb +1 -1
  22. data/lib/dor/indexers/identifiable_indexer.rb +0 -2
  23. data/lib/dor/indexers/processable_indexer.rb +55 -28
  24. data/lib/dor/indexers/releasable_indexer.rb +2 -2
  25. data/lib/dor/models/admin_policy_object.rb +4 -4
  26. data/lib/dor/models/concerns/assembleable.rb +4 -0
  27. data/lib/dor/models/concerns/contentable.rb +27 -69
  28. data/lib/dor/models/concerns/describable.rb +14 -29
  29. data/lib/dor/models/concerns/editable.rb +20 -334
  30. data/lib/dor/models/concerns/embargoable.rb +7 -11
  31. data/lib/dor/models/concerns/eventable.rb +5 -1
  32. data/lib/dor/models/concerns/geoable.rb +4 -4
  33. data/lib/dor/models/concerns/governable.rb +18 -87
  34. data/lib/dor/models/concerns/identifiable.rb +15 -75
  35. data/lib/dor/models/concerns/itemizable.rb +9 -11
  36. data/lib/dor/models/concerns/preservable.rb +4 -0
  37. data/lib/dor/models/concerns/processable.rb +30 -129
  38. data/lib/dor/models/concerns/publishable.rb +6 -55
  39. data/lib/dor/models/concerns/releaseable.rb +14 -227
  40. data/lib/dor/models/concerns/rightsable.rb +3 -3
  41. data/lib/dor/models/concerns/shelvable.rb +4 -49
  42. data/lib/dor/models/concerns/versionable.rb +21 -44
  43. data/lib/dor/models/set.rb +1 -1
  44. data/lib/dor/models/workflow_object.rb +2 -2
  45. data/lib/dor/services/ability.rb +77 -0
  46. data/lib/dor/services/cleanup_reset_service.rb +1 -3
  47. data/lib/dor/services/create_workflow_service.rb +51 -0
  48. data/lib/dor/services/creative_commons_license_service.rb +31 -0
  49. data/lib/dor/services/datastream_builder.rb +90 -0
  50. data/lib/dor/services/digital_stacks_service.rb +3 -21
  51. data/lib/dor/services/dublin_core_service.rb +40 -0
  52. data/lib/dor/services/file_metadata_merge_service.rb +67 -0
  53. data/lib/dor/services/indexing_service.rb +8 -4
  54. data/lib/dor/services/merge_service.rb +5 -5
  55. data/lib/dor/services/metadata_handlers/catalog_handler.rb +1 -1
  56. data/lib/dor/services/metadata_service.rb +6 -8
  57. data/lib/dor/{models/concerns → services}/mods2dc.xslt +0 -0
  58. data/lib/dor/services/ontology.rb +35 -0
  59. data/lib/dor/services/open_data_license_service.rb +20 -0
  60. data/lib/dor/services/public_desc_metadata_service.rb +21 -14
  61. data/lib/dor/services/public_xml_service.rb +6 -6
  62. data/lib/dor/services/publish_metadata_service.rb +100 -0
  63. data/lib/dor/services/registration_service.rb +43 -46
  64. data/lib/dor/services/release_tag_service.rb +251 -0
  65. data/lib/dor/services/reset_workspace_service.rb +1 -3
  66. data/lib/dor/services/sdr_ingest_service.rb +5 -7
  67. data/lib/dor/services/search_service.rb +10 -10
  68. data/lib/dor/services/secondary_file_name_service.rb +10 -0
  69. data/lib/dor/services/shelving_service.rb +67 -0
  70. data/lib/dor/services/status_service.rb +121 -0
  71. data/lib/dor/services/suri_service.rb +3 -5
  72. data/lib/dor/services/tag_service.rb +100 -0
  73. data/lib/dor/services/technical_metadata_service.rb +5 -4
  74. data/lib/dor/services/version_service.rb +84 -0
  75. data/lib/dor/utils/ng_tidy.rb +1 -1
  76. data/lib/dor/utils/sdr_client.rb +25 -9
  77. data/lib/dor/version.rb +1 -1
  78. data/lib/dor/workflow/document.rb +13 -13
  79. data/lib/dor/workflow/process.rb +71 -26
  80. data/lib/tasks/rdoc.rake +1 -1
  81. metadata +77 -51
  82. data/config/certs/robots-dor-dev.crt +0 -29
  83. data/config/certs/robots-dor-dev.key +0 -27
  84. data/config/dev_console_env.rb +0 -80
@@ -17,18 +17,18 @@ module Dor
17
17
  client = Config.fedora.client['risearch']
18
18
  client.options[:timeout] = opts.delete(:timeout)
19
19
  query_params = {
20
- :type => 'tuples',
21
- :lang => 'itql',
22
- :format => 'CSV',
23
- :limit => '1000',
24
- :stream => 'on',
25
- :query => query
20
+ type: 'tuples',
21
+ lang: 'itql',
22
+ format: 'CSV',
23
+ limit: '1000',
24
+ stream: 'on',
25
+ query: query
26
26
  }.merge(opts)
27
27
  result = client.post(query_params)
28
28
  result.split(/\n/)[1..-1].collect { |pid| pid.chomp.sub(/^info:fedora\//, '') }
29
29
  end
30
30
 
31
- def iterate_over_pids(opts = {}, &block)
31
+ def iterate_over_pids(opts = {})
32
32
  opts[:query] ||= 'select $object from <#ri> where $object <info:fedora/fedora-system:def/model#label> $label'
33
33
  opts[:in_groups_of] ||= 100
34
34
  opts[:mode] ||= :single
@@ -46,7 +46,7 @@ module Dor
46
46
  end
47
47
 
48
48
  def query(query, args = {})
49
- params = args.merge({ :q => query })
49
+ params = args.merge(q: query)
50
50
  params[:start] ||= 0
51
51
  resp = solr.get 'select', params: params
52
52
  return resp unless block_given?
@@ -68,7 +68,7 @@ module Dor
68
68
  end
69
69
  q = "{!term f=#{Solrizer.solr_name 'identifier', :symbol}}#{id}"
70
70
  result = []
71
- query(q, :fl => 'id', :rows => 1000, :defType => 'lucene') do |resp|
71
+ query(q, fl: 'id', rows: 1000, defType: 'lucene') do |resp|
72
72
  result += resp['response']['docs'].collect { |doc| doc['id'] }
73
73
  true
74
74
  end
@@ -86,7 +86,7 @@ module Dor
86
86
  end
87
87
 
88
88
  def find_sdr_graveyard_apo_druid
89
- r = Dor::SearchService.query('dc_title_tesim:"SDR Graveyard"', :fl => 'id')
89
+ r = Dor::SearchService.query('dc_title_tesim:"SDR Graveyard"', fl: 'id')
90
90
  if r['response']['docs'].empty?
91
91
  nil
92
92
  else
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dor
4
+ # Merges contentMetadata from several objects into one.
5
+ class SecondaryFileNameService
6
+ def self.create(old_name, sequence_num)
7
+ old_name =~ /^(.*)\.(.*)$/ ? "#{Regexp.last_match(1)}_#{sequence_num}.#{Regexp.last_match(2)}" : "#{old_name}_#{sequence_num}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dor
4
+ # Push file changes for shelve-able files into the stacks
5
+ class ShelvingService
6
+ def self.shelve(work)
7
+ new(work).shelve
8
+ end
9
+
10
+ def initialize(work)
11
+ @work = work
12
+ end
13
+
14
+ def shelve
15
+ # retrieve the differences between the current contentMetadata and the previously ingested version
16
+ diff = shelve_diff
17
+ stacks_object_pathname = stacks_location
18
+ # determine the location of the object's files in the stacks area
19
+ stacks_druid = DruidTools::StacksDruid.new work.id, stacks_object_pathname
20
+ stacks_object_pathname = Pathname(stacks_druid.path)
21
+ # determine the location of the object's content files in the workspace area
22
+ workspace_druid = DruidTools::Druid.new(work.id, Config.stacks.local_workspace_root)
23
+ workspace_content_pathname = workspace_content_dir(diff, workspace_druid)
24
+ # delete, rename, or copy files to the stacks area
25
+ DigitalStacksService.remove_from_stacks(stacks_object_pathname, diff)
26
+ DigitalStacksService.rename_in_stacks(stacks_object_pathname, diff)
27
+ DigitalStacksService.shelve_to_stacks(workspace_content_pathname, stacks_object_pathname, diff)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :work
33
+
34
+ # retrieve the differences between the current contentMetadata and the previously ingested version
35
+ # (filtering to select only the files that should be shelved to stacks)
36
+ def shelve_diff
37
+ raise Dor::ParameterError, 'Missing Dor::Config.stacks.local_workspace_root' if Config.stacks.local_workspace_root.nil?
38
+ raise Dor::Exception, 'Missing contentMetadata datastream' if work.contentMetadata.nil?
39
+
40
+ inventory_diff = Sdr::Client.get_content_diff(work.pid, work.contentMetadata.content, 'shelve')
41
+ inventory_diff.group_difference('content')
42
+ end
43
+
44
+ # Find the location of the object's content files in the workspace area
45
+ # @param [Moab::FileGroupDifference] content_diff The differences between the current contentMetadata and the previously ingested version
46
+ # @param [DruidTools::Druid] workspace_druid the location of the object's files in the workspace area
47
+ # @return [Pathname] The location of the object's content files in the workspace area
48
+ def workspace_content_dir(content_diff, workspace_druid)
49
+ deltas = content_diff.file_deltas
50
+ filelist = deltas[:modified] + deltas[:added] + deltas[:copyadded].collect { |_old, new| new }
51
+ return nil if filelist.empty?
52
+
53
+ Pathname(workspace_druid.find_filelist_parent('content', filelist))
54
+ end
55
+
56
+ # get the stack location based on the contentMetadata stacks attribute
57
+ # or using the default value from the config file if it doesn't exist
58
+ def stacks_location
59
+ return Config.stacks.local_stacks_root unless work.contentMetadata&.stacks.present?
60
+
61
+ location = work.contentMetadata.stacks[0]
62
+ return location if location.start_with? '/' # Absolute stacks path
63
+
64
+ raise "stacks attribute for item: #{work.id} contentMetadata should start with /. The current value is #{location}"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dor
4
+ # Query the processing status of an item.
5
+ # This has a dependency on the workflow service (app) to get milestones.
6
+ class StatusService
7
+ # verbiage we want to use to describe an item when it has completed a particular step
8
+ STATUS_CODE_DISP_TXT = {
9
+ 0 => 'Unknown Status', # if there are no milestones for the current version, someone likely messed up the versioning process.
10
+ 1 => 'Registered',
11
+ 2 => 'In accessioning',
12
+ 3 => 'In accessioning (described)',
13
+ 4 => 'In accessioning (described, published)',
14
+ 5 => 'In accessioning (described, published, deposited)',
15
+ 6 => 'Accessioned',
16
+ 7 => 'Accessioned (indexed)',
17
+ 8 => 'Accessioned (indexed, ingested)',
18
+ 9 => 'Opened'
19
+ }.freeze
20
+
21
+ # milestones from accessioning and the order they happen in
22
+ STEPS = {
23
+ 'registered' => 1,
24
+ 'submitted' => 2,
25
+ 'described' => 3,
26
+ 'published' => 4,
27
+ 'deposited' => 5,
28
+ 'accessioned' => 6,
29
+ 'indexed' => 7,
30
+ 'shelved' => 8,
31
+ 'opened' => 9
32
+ }.freeze
33
+
34
+ # @return [Hash{Symbol => Object}] including :current_version, :status_code and :status_time
35
+ def self.status_info(work)
36
+ new(work).status_info
37
+ end
38
+
39
+ def self.status(work, include_time = false)
40
+ new(work).status(include_time)
41
+ end
42
+
43
+ def initialize(work)
44
+ @work = work
45
+ end
46
+
47
+ # @return [Hash{Symbol => Object}] including :current_version, :status_code and :status_time
48
+ def status_info
49
+ status_code = 0
50
+ status_time = nil
51
+ # for each milestone in the current version, see if it comes after the current 'last' step, if so, make it the last and record the date/time
52
+ current_milestones.each do |m|
53
+ m_name = m[:milestone]
54
+ m_time = m[:at].utc.xmlschema
55
+ next unless STEPS.key?(m_name) && (!status_time || m_time > status_time)
56
+
57
+ status_code = STEPS[m_name]
58
+ status_time = m_time
59
+ end
60
+
61
+ { current_version: current_version, status_code: status_code, status_time: status_time }
62
+ end
63
+
64
+ # @param [Boolean] include_time
65
+ # @return [String] single composed status from status_info
66
+ def status(include_time = false)
67
+ status_info_hash = status_info
68
+ current_version = status_info_hash[:current_version]
69
+ status_code = status_info_hash[:status_code]
70
+ status_time = status_info_hash[:status_time]
71
+
72
+ # use the translation table to get the appropriate verbage for the latest step
73
+ result = "v#{current_version} #{STATUS_CODE_DISP_TXT[status_code]}"
74
+ result += " #{format_date(status_time)}" if include_time
75
+ result
76
+ end
77
+
78
+ def milestones
79
+ @milestones ||= Dor::Config.workflow.client.get_milestones('dor', work.pid)
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :work
85
+
86
+ def current_version
87
+ @current_version ||= begin
88
+ work.versionMetadata.current_version_id
89
+ rescue StandardError
90
+ '1'
91
+ end
92
+ end
93
+
94
+ def current_milestones
95
+ current = []
96
+ # only get steps that are part of accessioning and part of the current version. That can mean they were archived with the current version
97
+ # number, or they might be active (no version number).
98
+ milestones.each do |m|
99
+ if STEPS.key?(m[:milestone]) && (m[:version].nil? || m[:version] == current_version)
100
+ current << m unless m[:milestone] == 'registered' && current_version.to_i > 1
101
+ end
102
+ end
103
+ current
104
+ end
105
+
106
+ # handles formating utc date/time to human readable
107
+ # XXX: bad form to hardcode TZ here.
108
+ def format_date(datetime)
109
+ d =
110
+ if datetime.is_a?(Time)
111
+ datetime
112
+ else
113
+ DateTime.parse(datetime).in_time_zone(ActiveSupport::TimeZone.new('Pacific Time (US & Canada)'))
114
+ end
115
+ I18n.l(d).strftime('%Y-%m-%d %I:%M%p')
116
+ rescue StandardError
117
+ d = datetime.is_a?(Time) ? datetime : Time.parse(datetime.to_s)
118
+ d.strftime('%Y-%m-%d %I:%M%p')
119
+ end
120
+ end
121
+ end
@@ -15,18 +15,16 @@ module Dor
15
15
  if Config.suri.mint_ids
16
16
  # Post with no body
17
17
  resource = RestClient::Resource.new("#{Config.suri.url}/suri2/namespaces/#{Config.suri.id_namespace}",
18
- :user => Config.suri.user, :password => Config.suri.pass)
18
+ user: Config.suri.user, password: Config.suri.pass)
19
19
  ids = resource["identifiers?quantity=#{quantity}"].post('').chomp.split(/\n/).collect { |id| "#{Config.suri.id_namespace}:#{id.strip}" }
20
20
  else
21
21
  repo = ActiveFedora::Base.respond_to?(:connection_for_pid) ? ActiveFedora::Base.connection_for_pid(0) : ActiveFedora.fedora.connection
22
22
  resp = Nokogiri::XML(repo.api.next_pid(numPIDs: quantity))
23
- ids = resp.xpath('/pidList/pid').collect { |node| node.text }
23
+ ids = resp.xpath('/pidList/pid').collect(&:text)
24
24
  # With modernish (circa 2015/6) dependencies, including Nokogiri and
25
25
  # ActiveFedora/Rubydora, `ids` is `[]` above. If that is the case, try
26
26
  # the XPath that works (confirmed with most recent `hydra_etd` work)
27
- if ids.empty? && resp.root.namespaces.any?
28
- ids = resp.xpath('/xmlns:pidList/xmlns:pid').collect { |node| node.text }
29
- end
27
+ ids = resp.xpath('/xmlns:pidList/xmlns:pid').collect(&:text) if ids.empty? && resp.root.namespaces.any?
30
28
  end
31
29
  want_array ? ids : ids.first
32
30
 
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dor
4
+ # Manage tags on an object
5
+ class TagService
6
+ def self.add(item, tag)
7
+ new(item).add(tag)
8
+ end
9
+
10
+ def self.remove(item, tag)
11
+ new(item).remove(tag)
12
+ end
13
+
14
+ def self.update(item, old_tag, new_tag)
15
+ new(item).update(old_tag, new_tag)
16
+ end
17
+
18
+ def initialize(item)
19
+ @item = item
20
+ end
21
+
22
+ # Add an administrative tag to an item, you will need to seperately save the item to write it to fedora
23
+ # @param tag [string] The tag you wish to add
24
+ def add(tag)
25
+ normalized_tag = validate_and_normalize_tag(tag, identity_metadata.tags)
26
+ identity_metadata.add_value(:tag, normalized_tag)
27
+ end
28
+
29
+ def remove(tag)
30
+ normtag = normalize_tag(tag)
31
+ tag_nodes
32
+ .select { |node| normalize_tag(node.content) == normtag }
33
+ .each { identity_metadata.ng_xml_will_change! }
34
+ .each(&:remove)
35
+ .any?
36
+ end
37
+
38
+ def update(old_tag, new_tag)
39
+ normtag = normalize_tag(old_tag)
40
+ tag_nodes
41
+ .select { |node| normalize_tag(node.content) == normtag }
42
+ .each { identity_metadata.ng_xml_will_change! }
43
+ .each { |node| node.content = normalize_tag(new_tag) }
44
+ .any?
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :item
50
+ def identity_metadata
51
+ item.identityMetadata
52
+ end
53
+
54
+ def tag_nodes
55
+ identity_metadata.ng_xml.search('//tag')
56
+ end
57
+
58
+ # turns a tag string into an array with one element per tag part.
59
+ # split on ":", disregard leading and trailing whitespace on tokens.
60
+ def split_tag_to_arr(tag_str)
61
+ tag_str.split(':').map(&:strip)
62
+ end
63
+
64
+ # turn a tag array back into a tag string with a standard format
65
+ def normalize_tag_arr(tag_arr)
66
+ tag_arr.join(' : ')
67
+ end
68
+
69
+ # take a tag string and return a normalized tag string
70
+ def normalize_tag(tag_str)
71
+ normalize_tag_arr(split_tag_to_arr(tag_str))
72
+ end
73
+
74
+ # take a proposed tag string and a list of the existing tags for the object being edited. if
75
+ # the proposed tag is valid, return it in normalized form. if not, raise an exception with an
76
+ # explanatory message.
77
+ def validate_and_normalize_tag(tag_str, existing_tag_list)
78
+ tag_arr = validate_tag_format(tag_str)
79
+
80
+ # note that the comparison for duplicate tags is case-insensitive, but we don't change case as part of the normalized version
81
+ # we return, because we want to preserve the user's intended case.
82
+ normalized_tag = normalize_tag_arr(tag_arr)
83
+ dupe_existing_tag = existing_tag_list.detect { |existing_tag| normalize_tag(existing_tag).casecmp(normalized_tag) == 0 }
84
+ raise "An existing tag (#{dupe_existing_tag}) is the same, consider using update_tag?" if dupe_existing_tag
85
+
86
+ normalized_tag
87
+ end
88
+
89
+ # Ensure that an administrative tag meets the proper mininum format
90
+ # @param tag_str [String] the tag
91
+ # @return [Array] the tag split into an array via ':'
92
+ def validate_tag_format(tag_str)
93
+ tag_arr = split_tag_to_arr(tag_str)
94
+ raise ArgumentError, "Invalid tag structure: tag '#{tag_str}' must have at least 2 elements" if tag_arr.length < 2
95
+ raise ArgumentError, "Invalid tag structure: tag '#{tag_str}' contains empty elements" if tag_arr.detect(&:empty?)
96
+
97
+ tag_arr
98
+ end
99
+ end
100
+ end
@@ -49,10 +49,11 @@ module Dor
49
49
  # @param [Dor::Item] dor_item The DOR item being processed by the technical metadata robot
50
50
  # @return [FileGroupDifference] The differences between two versions of a group of files
51
51
  def self.get_content_group_diff(dor_item)
52
- inventory_diff = dor_item.get_content_diff('all')
52
+ return Moab::FileGroupDifference.new if dor_item.contentMetadata.nil?
53
+ raise Dor::ParameterError, 'Missing Dor::Config.stacks.local_workspace_root' if Config.stacks.local_workspace_root.nil?
54
+
55
+ inventory_diff = Sdr::Client.get_content_diff(dor_item.pid, dor_item.contentMetadata.content, 'all')
53
56
  inventory_diff.group_difference('content')
54
- rescue Dor::Exception # no contentMetadata
55
- Moab::FileGroupDifference.new
56
57
  end
57
58
 
58
59
  # @param [FileGroupDifference] content_group_diff
@@ -92,7 +93,7 @@ module Dor
92
93
  # The data is updated to the latest format.
93
94
  def self.get_dor_technical_metadata(dor_item)
94
95
  ds = 'technicalMetadata'
95
- return nil unless dor_item.datastreams.keys.include?(ds) && !dor_item.datastreams[ds].new?
96
+ return nil unless dor_item.datastreams.key?(ds) && !dor_item.datastreams[ds].new?
96
97
 
97
98
  dor_techmd = dor_item.datastreams[ds].content
98
99
  return dor_techmd if dor_techmd =~ /<technicalMetadata/
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dor
4
+ # Open and close versions
5
+ class VersionService
6
+ def self.open(work, opts = {})
7
+ new(work).open(opts)
8
+ end
9
+
10
+ def self.close(work, opts = {})
11
+ new(work).close(opts)
12
+ end
13
+
14
+ def initialize(work)
15
+ @work = work
16
+ end
17
+
18
+ # Increments the version number and initializes versioningWF for the object
19
+ # @param [Hash] opts optional params
20
+ # @option opts [Boolean] :assume_accessioned If true, does not check whether object has been accessioned.
21
+ # @option opts [Boolean] :create_workflows_ds If false, create_workflow() will not initialize the workflows datastream.
22
+ # @option opts [Hash] :vers_md_upd_info If present, used to add to the events datastream and set the desc and significance on the versionMetadata datastream
23
+ # @raise [Dor::Exception] if the object hasn't been accessioned, or if a version is already opened
24
+ def open(opts = {})
25
+ # During local development, we need a way to open a new version even if the object has not been accessioned.
26
+ raise(Dor::Exception, 'Object net yet accessioned') unless
27
+ opts[:assume_accessioned] || Dor::Config.workflow.client.get_lifecycle('dor', work.pid, 'accessioned')
28
+ raise Dor::Exception, 'Object already opened for versioning' if open?
29
+ raise Dor::Exception, 'Object currently being accessioned' if Dor::Config.workflow.client.get_active_lifecycle('dor', work.pid, 'submitted')
30
+
31
+ sdr_version = Sdr::Client.current_version work.pid
32
+
33
+ vmd_ds = work.versionMetadata
34
+ vmd_ds.sync_then_increment_version sdr_version
35
+ vmd_ds.save unless work.new_record?
36
+
37
+ k = :create_workflows_ds
38
+ if opts.key?(k)
39
+ # During local development, Hydrus (or another app w/ local Fedora) does not want to initialize workflows datastream.
40
+ work.create_workflow('versioningWF', opts[k])
41
+ else
42
+ work.create_workflow('versioningWF')
43
+ end
44
+
45
+ vmd_upd_info = opts[:vers_md_upd_info]
46
+ return unless vmd_upd_info
47
+
48
+ work.events.add_event('open', vmd_upd_info[:opening_user_name], "Version #{vmd_ds.current_version_id} opened")
49
+ vmd_ds.update_current_version(description: vmd_upd_info[:description], significance: vmd_upd_info[:significance].to_sym)
50
+ work.save
51
+ end
52
+
53
+ # Sets versioningWF:submit-version to completed and initiates accessionWF for the object
54
+ # @param [Hash] opts optional params
55
+ # @option opts [String] :description describes the version change
56
+ # @option opts [Symbol] :significance which part of the version tag to increment
57
+ # :major, :minor, :admin (see Dor::VersionTag#increment)
58
+ # @option opts [String] :version_num version number to archive rows with. Otherwise, current version is used
59
+ # @option opts [Boolean] :start_accesion set to true if you want accessioning to start (default), false otherwise
60
+ # @raise [Dor::Exception] if the object hasn't been opened for versioning, or if accessionWF has
61
+ # already been instantiated or the current version is missing a tag or description
62
+ def close(opts = {})
63
+ unless opts.empty?
64
+ work.versionMetadata.update_current_version opts
65
+ work.versionMetadata.save
66
+ end
67
+
68
+ raise Dor::Exception, 'latest version in versionMetadata requires tag and description before it can be closed' unless work.versionMetadata.current_version_closeable?
69
+ raise Dor::Exception, 'Trying to close version on an object not opened for versioning' unless open?
70
+ raise Dor::Exception, 'accessionWF already created for versioned object' if Dor::Config.workflow.client.get_active_lifecycle('dor', work.pid, 'submitted')
71
+
72
+ Dor::Config.workflow.client.close_version 'dor', work.pid, opts.fetch(:start_accession, true) # Default to creating accessionWF when calling close_version
73
+ end
74
+
75
+ # @return [Boolean] true if 'opened' lifecycle is active, false otherwise
76
+ def open?
77
+ return true if Dor::Config.workflow.client.get_active_lifecycle('dor', work.pid, 'opened')
78
+
79
+ false
80
+ end
81
+
82
+ attr_reader :work
83
+ end
84
+ end