dor-services 6.8.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/config/config_defaults.yml +0 -27
  3. data/config/dev_console_env.rb.example +0 -17
  4. data/lib/dor-services.rb +9 -73
  5. data/lib/dor/config.rb +1 -30
  6. data/lib/dor/datastreams/content_metadata_ds.rb +8 -0
  7. data/lib/dor/datastreams/desc_metadata_ds.rb +19 -0
  8. data/lib/dor/datastreams/identity_metadata_ds.rb +65 -0
  9. data/lib/dor/datastreams/rights_metadata_ds.rb +14 -2
  10. data/lib/dor/datastreams/workflow_definition_ds.rb +1 -1
  11. data/lib/dor/datastreams/workflow_ds.rb +0 -15
  12. data/lib/dor/indexers/identifiable_indexer.rb +8 -4
  13. data/lib/dor/indexers/releasable_indexer.rb +7 -1
  14. data/lib/dor/models/abstract.rb +143 -8
  15. data/lib/dor/models/admin_policy_object.rb +0 -3
  16. data/lib/dor/models/collection.rb +0 -2
  17. data/lib/dor/models/concerns/embargoable.rb +7 -60
  18. data/lib/dor/models/etd.rb +100 -0
  19. data/lib/dor/models/item.rb +12 -28
  20. data/lib/dor/models/part.rb +18 -0
  21. data/lib/dor/models/set.rb +0 -2
  22. data/lib/dor/services/collection_service.rb +36 -0
  23. data/lib/dor/services/embargo_service.rb +93 -0
  24. data/lib/dor/services/ontology.rb +0 -18
  25. data/lib/dor/services/public_desc_metadata_service.rb +7 -11
  26. data/lib/dor/services/search_service.rb +0 -40
  27. data/lib/dor/version.rb +1 -1
  28. data/lib/dor/workflow/document.rb +0 -7
  29. metadata +15 -78
  30. data/lib/dor/models/concerns/assembleable.rb +0 -18
  31. data/lib/dor/models/concerns/contentable.rb +0 -185
  32. data/lib/dor/models/concerns/describable.rb +0 -82
  33. data/lib/dor/models/concerns/eventable.rb +0 -18
  34. data/lib/dor/models/concerns/geoable.rb +0 -14
  35. data/lib/dor/models/concerns/governable.rb +0 -101
  36. data/lib/dor/models/concerns/identifiable.rb +0 -172
  37. data/lib/dor/models/concerns/itemizable.rb +0 -42
  38. data/lib/dor/models/concerns/preservable.rb +0 -46
  39. data/lib/dor/models/concerns/processable.rb +0 -86
  40. data/lib/dor/models/concerns/publishable.rb +0 -76
  41. data/lib/dor/models/concerns/releaseable.rb +0 -118
  42. data/lib/dor/models/concerns/rightsable.rb +0 -25
  43. data/lib/dor/models/concerns/shelvable.rb +0 -15
  44. data/lib/dor/models/concerns/versionable.rb +0 -72
  45. data/lib/dor/services/ability.rb +0 -77
  46. data/lib/dor/services/cleanup_reset_service.rb +0 -103
  47. data/lib/dor/services/datastream_builder.rb +0 -96
  48. data/lib/dor/services/decommission_service.rb +0 -31
  49. data/lib/dor/services/digital_stacks_service.rb +0 -125
  50. data/lib/dor/services/dublin_core_service.rb +0 -45
  51. data/lib/dor/services/file_metadata_merge_service.rb +0 -71
  52. data/lib/dor/services/indexing_service.rb +0 -131
  53. data/lib/dor/services/merge_service.rb +0 -105
  54. data/lib/dor/services/public_xml_service.rb +0 -116
  55. data/lib/dor/services/publish_metadata_service.rb +0 -99
  56. data/lib/dor/services/reset_workspace_service.rb +0 -27
  57. data/lib/dor/services/sdr_ingest_service.rb +0 -172
  58. data/lib/dor/services/secondary_file_name_service.rb +0 -10
  59. data/lib/dor/services/shelving_service.rb +0 -69
  60. data/lib/dor/services/technical_metadata_service.rb +0 -232
  61. data/lib/dor/services/version_service.rb +0 -84
  62. data/lib/dor/utils/sdr_client.rb +0 -94
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dor
4
- class Ability
5
- class << self
6
- def can_manage_item?(roles)
7
- intersect roles, groups_which_manage_item
8
- end
9
-
10
- def can_manage_desc_metadata?(roles)
11
- intersect roles, groups_which_manage_desc_metadata
12
- end
13
-
14
- def can_manage_system_metadata?(roles)
15
- intersect roles, groups_which_manage_system_metadata
16
- end
17
-
18
- def can_manage_content?(roles)
19
- intersect roles, groups_which_manage_content
20
- end
21
-
22
- def can_manage_rights?(roles)
23
- intersect roles, groups_which_manage_rights
24
- end
25
-
26
- def can_manage_embargo?(roles)
27
- intersect roles, groups_which_manage_embargo
28
- end
29
-
30
- def can_view_content?(roles)
31
- intersect roles, groups_which_view_content
32
- end
33
-
34
- def can_view_metadata?(roles)
35
- intersect roles, groups_which_view_metadata
36
- end
37
-
38
- private
39
-
40
- def groups_which_manage_item
41
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor']
42
- end
43
-
44
- def groups_which_manage_desc_metadata
45
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor', 'dor-apo-metadata']
46
- end
47
-
48
- def groups_which_manage_system_metadata
49
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor']
50
- end
51
-
52
- def groups_which_manage_content
53
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor']
54
- end
55
-
56
- def groups_which_manage_rights
57
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor']
58
- end
59
-
60
- def groups_which_manage_embargo
61
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor']
62
- end
63
-
64
- def groups_which_view_content
65
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor', 'dor-viewer', 'sdr-viewer']
66
- end
67
-
68
- def groups_which_view_metadata
69
- ['dor-administrator', 'sdr-administrator', 'dor-apo-manager', 'dor-apo-depositor', 'dor-viewer', 'sdr-viewer']
70
- end
71
-
72
- def intersect(arr1, arr2)
73
- (arr1 & arr2).length > 0
74
- end
75
- end
76
- end
77
- end
@@ -1,103 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pathname'
4
-
5
- module Dor
6
- # Remove all traces of the object's data files from the workspace and export areas
7
- class CleanupResetService
8
- # @param [String] druid The identifier for the object whose reset data is to be removed
9
- # @return [void] remove copy of the reset data that was exported to preservation core
10
- def self.cleanup_by_reset_druid(druid)
11
- last_version = get_druid_last_version(druid)
12
- cleanup_reset_workspace_content(druid, last_version, Config.cleanup.local_workspace_root)
13
- cleanup_assembly_content(druid, Config.cleanup.local_assembly_root)
14
- cleanup_reset_export(druid, last_version)
15
- end
16
-
17
- def self.get_druid_last_version(druid)
18
- druid_obj = Dor.find(druid)
19
- last_version = druid_obj.current_version.to_i
20
-
21
- # if the current version is still open, avoid this versioned directory
22
- last_version -= 1 if Dor::Config.workflow.client.lifecycle('dor', druid, 'accessioned').nil?
23
- last_version
24
- end
25
-
26
- # @param [String] druid The identifier for the object whose reset data is to be removed
27
- # @param [String] base The base directory to delete from
28
- # @param [Integer] last_version The last version that the data should be removed until version 1
29
- # @return [void] remove all the object's reset data files from the workspace area equal to less than the last_version
30
- def self.cleanup_reset_workspace_content(druid, last_version, base)
31
- base_druid = DruidTools::Druid.new(druid, base)
32
- base_druid_tree = base_druid.pathname.to_s
33
- # if it is truncated tree /aa/111/aaa/1111/content,
34
- # we should follow the regular cleanup technique
35
-
36
- reset_directories = get_reset_dir_list(last_version, base_druid_tree)
37
- reset_directories.each do |path|
38
- FileUtils.rm_rf(path)
39
- end
40
- base_druid.prune_ancestors(base_druid.pathname.parent)
41
- end
42
-
43
- # @param [String] base_druid_tree The base directory to delete from
44
- # @param [Integer] last_version The last version that the data should be removed until version 1
45
- # @return [void] prepares a list of reset directories that should be removed
46
- def self.get_reset_dir_list(last_version, base_druid_tree)
47
- reset_directories = []
48
- (1..last_version).each do |i|
49
- reset_path = "#{base_druid_tree}_v#{i}"
50
- reset_directories.append(reset_path) if File.exist?(reset_path)
51
- end
52
- reset_directories
53
- end
54
-
55
- # @param [String] druid The identifier for the object whose reset bags data is to be removed
56
- # @return [void] remove copy of the reset data that was exported to preservation core
57
- def self.cleanup_reset_export(druid, last_version)
58
- id = druid.split(':').last
59
- base_bag_directory = File.join(Config.cleanup.local_export_home, id)
60
-
61
- bag_dir_list = get_reset_bag_dir_list(last_version, base_bag_directory)
62
- bag_dir_list.each do |bag_dir|
63
- Pathname(bag_dir).rmtree
64
- end
65
-
66
- bag_tar_list = get_reset_bag_tar_list(last_version, base_bag_directory)
67
- bag_tar_list.each do |bag_tar|
68
- Pathname(bag_tar).rmtree
69
- end
70
- end
71
-
72
- # @param [Integer] last_version The last version that the data should be removed until version 1
73
- # @param [String] base_bag_directory The base bag directory including the export home and druid id
74
- # @return [void] prepares a list of reset bag directories that should be removed
75
- def self.get_reset_bag_dir_list(last_version, base_bag_directory)
76
- reset_bags = []
77
- (1..last_version).each do |i|
78
- reset_path = "#{base_bag_directory}_v#{i}"
79
- reset_bags.append(reset_path) if File.exist?(reset_path)
80
- end
81
- reset_bags
82
- end
83
-
84
- # @param [String] base_bag_directory The base bag directory including the export home and druid id
85
- # @param [Integer] last_version The last version that the data should be removed until version 1
86
- # @return [void] prepares a list of reset bag tars that should be removed
87
- def self.get_reset_bag_tar_list(last_version, base_bag_directory)
88
- reset_bags = []
89
- (1..last_version).each do |i|
90
- reset_path = "#{base_bag_directory}_v#{i}.tar"
91
- reset_bags.append(reset_path) if File.exist?(reset_path)
92
- end
93
- reset_bags
94
- end
95
-
96
- # @param [String] druid The identifier for the object whose data is to be removed
97
- # @param [String] base The base directory to delete from
98
- # @return [void] remove the object's data files from the assembly area
99
- def self.cleanup_assembly_content(druid, base)
100
- DruidTools::Druid.new(druid, base).prune!
101
- end
102
- end
103
- end
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dor
4
- # The ContentMetadata and DescMetadata robot are allowed to build the
5
- # datastream by reading a file from the /dor/workspace that matches the
6
- # datastream name. This allows assembly or pre-assembly to prebuild the
7
- # datastreams from templates or using other means
8
- # (like the assembly-objectfile gem) and then have those datastreams picked
9
- # up and added to the object during accessionWF.
10
- #
11
- # This class builds that datastream using the content of a file if such a file
12
- # exists and is newer than the object's current datastream (see above); otherwise,
13
- # builds the datastream by calling build_fooMetadata_datastream.
14
- class DatastreamBuilder
15
- # @param [ActiveFedora::Base] object The object that contains the datastream
16
- # @param [ActiveFedora::Datastream] datastream The datastream object
17
- # @param [Boolean] force Should we overwrite existing datastream?
18
- # @param [Boolean] required If set to true, raise an error if we can't build the datastream
19
- # @return [ActiveFedora::Datastream]
20
- def initialize(object:, datastream:, force: false, required: false)
21
- @object = object
22
- @datastream = datastream
23
- @force = force
24
- @required = required
25
- raise 'Datastream required, but none provided' if required && !datastream
26
- end
27
-
28
- def build
29
- return unless datastream
30
-
31
- # See if datastream exists as a file and if the file's timestamp is newer than datastream's timestamp.
32
- if file_newer_than_datastream?
33
- create_from_file(filename)
34
- elsif force || empty_datastream?
35
- create_default
36
- end
37
- # Check for success.
38
- raise "Required datastream #{datastream_name} could not be populated!" if required && empty_datastream?
39
- end
40
-
41
- private
42
-
43
- attr_reader :datastream, :force, :object, :required
44
-
45
- def filename
46
- @filename ||= find_metadata_file
47
- end
48
-
49
- # @return [String] datastream name (dsid)
50
- def datastream_name
51
- datastream.dsid
52
- end
53
-
54
- def file_newer_than_datastream?
55
- filename && (!datastream_date || file_date > datastream_date)
56
- end
57
-
58
- def file_date
59
- File.mtime(filename)
60
- end
61
-
62
- def datastream_date
63
- datastream.createDate
64
- end
65
-
66
- def create_from_file(filename)
67
- content = File.read(filename)
68
- datastream.content = content
69
- datastream.ng_xml = Nokogiri::XML(content) if datastream.respond_to?(:ng_xml)
70
- datastream.save unless datastream.digital_object.new?
71
- end
72
-
73
- def create_default
74
- meth = "build_#{datastream_name}_datastream".to_sym
75
- return unless object.respond_to?(meth)
76
-
77
- object.public_send(meth, datastream)
78
- datastream.save unless datastream.digital_object.new?
79
- end
80
-
81
- # Tries to find a file for the datastream.
82
- # @return [String, nil] path to datastream or nil
83
- def find_metadata_file
84
- druid = DruidTools::Druid.new(object.pid, Dor::Config.stacks.local_workspace_root)
85
- druid.find_metadata("#{datastream_name}.xml")
86
- end
87
-
88
- def empty_datastream?
89
- return true if datastream.new?
90
-
91
- return datastream.content.to_s.empty? unless datastream.class.respond_to?(:xml_template)
92
-
93
- datastream.content.to_s.empty? || EquivalentXml.equivalent?(datastream.content, datastream.class.xml_template)
94
- end
95
- end
96
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dor
4
- # Responsible for decommissioning objects
5
- class DecommissionService
6
- # @param [Dor::Item] object
7
- def initialize(object)
8
- @object = object
9
- end
10
-
11
- attr_reader :object
12
-
13
- # Clears RELS-EXT relationships, sets the isGovernedBy relationship to the SDR Graveyard APO
14
- # @param [String] tag optional String of text that is concatenated to the identityMetadata/tag "Decommissioned : "
15
- def decommission(tag)
16
- # remove isMemberOf and isMemberOfCollection relationships
17
- object.clear_relationship :is_member_of
18
- object.clear_relationship :is_member_of_collection
19
- # remove isGovernedBy relationship
20
- object.clear_relationship :is_governed_by
21
- # add isGovernedBy to graveyard APO druid:sw909tc7852
22
- # SEARCH BY dc title for 'SDR Graveyard'
23
- object.add_relationship :is_governed_by, ActiveFedora::Base.find(Dor::SearchService.sdr_graveyard_apo_druid)
24
- # eliminate contentMetadata. set it to <contentMetadata/> ?
25
- object.contentMetadata.content = '<contentMetadata/>'
26
- # eliminate rightsMetadata. set it to <rightsMetadata/> ?
27
- object.rightsMetadata.content = '<rightsMetadata/>'
28
- TagService.add object, "Decommissioned : #{tag}"
29
- end
30
- end
31
- end
@@ -1,125 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/ssh'
4
- require 'net/sftp'
5
-
6
- module Dor
7
- class DigitalStacksService
8
- # Delete files from stacks that have change type 'deleted', 'copydeleted', or 'modified'
9
- # @param [Pathname] stacks_object_pathname the stacks location of the digital object
10
- # @param [Moab::FileGroupDifference] content_diff the content file version differences report
11
- def self.remove_from_stacks(stacks_object_pathname, content_diff)
12
- %i[deleted copydeleted modified].each do |change_type|
13
- subset = content_diff.subset(change_type) # {Moab::FileGroupDifferenceSubset}
14
- subset.files.each do |moab_file| # {Moab::FileInstanceDifference}
15
- moab_signature = moab_file.signatures.first # {Moab::FileSignature}
16
- file_pathname = stacks_object_pathname.join(moab_file.basis_path)
17
- delete_file(file_pathname, moab_signature)
18
- end
19
- end
20
- end
21
-
22
- # Delete a file, but only if it exists and matches the expected signature
23
- # @param [Pathname] file_pathname The location of the file to be deleted
24
- # @param [Moab::FileSignature] moab_signature The fixity values of the file
25
- # @return [Boolean] true if file deleted, false otherwise
26
- def self.delete_file(file_pathname, moab_signature)
27
- if file_pathname.exist? && (file_pathname.size == moab_signature.size)
28
- file_signature = Moab::FileSignature.new.signature_from_file(file_pathname)
29
- if file_signature == moab_signature
30
- file_pathname.delete
31
- return true
32
- end
33
- end
34
- false
35
- end
36
-
37
- # Rename files from stacks that have change type 'renamed' using an intermediate temp filename.
38
- # The 2-step renaming allows chained or cyclic renames to occur without file collisions.
39
- # @param [Pathname] stacks_object_pathname the stacks location of the digital object
40
- # @param [Moab::FileGroupDifference] content_diff the content file version differences report
41
- def self.rename_in_stacks(stacks_object_pathname, content_diff)
42
- subset = content_diff.subset(:renamed) # {Moab::FileGroupDifferenceSubset
43
-
44
- # 1st Pass - rename files from original name to checksum-based name
45
- subset.files.each do |moab_file| # {Moab::FileInstanceDifference}
46
- moab_signature = moab_file.signatures.first # {Moab::FileSignature}
47
- original_pathname = stacks_object_pathname.join(moab_file.basis_path)
48
- temp_pathname = stacks_object_pathname.join(moab_signature.checksums.values.last)
49
- rename_file(original_pathname, temp_pathname, moab_signature)
50
- end
51
-
52
- # 2nd Pass - rename files from checksum-based name to new name
53
- subset.files.each do |moab_file| # {Moab::FileInstanceDifference}
54
- moab_signature = moab_file.signatures.first # {Moab::FileSignature}
55
- temp_pathname = stacks_object_pathname.join(moab_signature.checksums.values.last)
56
- new_pathname = stacks_object_pathname.join(moab_file.other_path)
57
- rename_file(temp_pathname, new_pathname, moab_signature)
58
- end
59
- end
60
-
61
- # Rename a file, but only if it exists and has the expected signature
62
- # @param [Pathname] old_pathname The original location/name of the file being renamed
63
- # @param [Pathname] new_pathname The new location/name of the file
64
- # @param [Moab::FileSignature] moab_signature The fixity values of the file
65
- # @return [Boolean] true if file renamed, false otherwise
66
- def self.rename_file(old_pathname, new_pathname, moab_signature)
67
- if old_pathname.exist? && (old_pathname.size == moab_signature.size)
68
- file_signature = Moab::FileSignature.new.signature_from_file(old_pathname)
69
- if file_signature == moab_signature
70
- new_pathname.parent.mkpath
71
- old_pathname.rename(new_pathname)
72
- return true
73
- end
74
- end
75
- false
76
- end
77
-
78
- # Add files to stacks that have change type 'added', 'copyadded' or 'modified'.
79
- # @param [Pathname] workspace_content_pathname The dor workspace location of the digital object's content fies
80
- # @param [Pathname] stacks_object_pathname the stacks location of the digital object's shelved files
81
- # @param [Moab::FileGroupDifference] content_diff the content file version differences report
82
- def self.shelve_to_stacks(workspace_content_pathname, stacks_object_pathname, content_diff)
83
- return false if workspace_content_pathname.nil?
84
-
85
- %i[added copyadded modified].each do |change_type|
86
- subset = content_diff.subset(change_type) # {Moab::FileGroupDifferenceSubset
87
- subset.files.each do |moab_file| # {Moab::FileInstanceDifference}
88
- moab_signature = moab_file.signatures.last # {Moab::FileSignature}
89
- filename = change_type == :modified ? moab_file.basis_path : moab_file.other_path
90
- workspace_pathname = workspace_content_pathname.join(filename)
91
- stacks_pathname = stacks_object_pathname.join(filename)
92
- copy_file(workspace_pathname, stacks_pathname, moab_signature)
93
- end
94
- end
95
- true
96
- end
97
-
98
- # Copy a file to stacks, but only if it does not yet exist with the expected signature
99
- # @param [Pathname] workspace_pathname The location of the file in the DOR workspace
100
- # @param [Pathname] stacks_pathname The location of the file in the stacks
101
- # @param [Moab::FileSignature] moab_signature The fixity values of the file
102
- # @return [Boolean] true if file copied, false otherwise
103
- def self.copy_file(workspace_pathname, stacks_pathname, moab_signature)
104
- if stacks_pathname.exist?
105
- file_signature = Moab::FileSignature.new.signature_from_file(stacks_pathname)
106
- stacks_pathname.delete if file_signature != moab_signature
107
- end
108
- unless stacks_pathname.exist?
109
- stacks_pathname.parent.mkpath
110
- FileUtils.cp workspace_pathname.to_s, stacks_pathname.to_s
111
- # Change permissions
112
- FileUtils.chmod 'u=rw,go=r', stacks_pathname.to_s
113
- return true
114
- end
115
- false
116
- end
117
-
118
- # Assumes the digital stacks storage root is mounted to the local file system
119
- # TODO: since this is delegating to the Druid, this method may not be necessary
120
- def self.prune_stacks_dir(id)
121
- stacks_druid_tree = DruidTools::StacksDruid.new(id, Config.stacks.local_stacks_root)
122
- stacks_druid_tree.prune!
123
- end
124
- end
125
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dor
4
- class DublinCoreService
5
- MODS_TO_DC_XSLT = Nokogiri::XSLT(File.new(File.expand_path(File.dirname(__FILE__) + '/mods2dc.xslt')))
6
- XMLNS_OAI_DC = 'http://www.openarchives.org/OAI/2.0/oai_dc/'
7
- class CrosswalkError < RuntimeError; end
8
-
9
- def initialize(work, include_collection_as_related_item: true)
10
- @work = work
11
- @include_collection = include_collection_as_related_item
12
- end
13
-
14
- # Generates Dublin Core from the MODS in the descMetadata datastream using the LoC mods2dc stylesheet
15
- # Should not be used for the Fedora DC datastream
16
- # @raise [CrosswalkError] Raises an Exception if the generated DC is empty or has no children
17
- # @return [Nokogiri::XML::Document] the DublinCore XML document object
18
- def ng_xml
19
- dc_doc = MODS_TO_DC_XSLT.transform(desc_md)
20
- dc_doc.xpath('/oai_dc:dc/*[count(text()) = 0]', oai_dc: XMLNS_OAI_DC).remove # Remove empty nodes
21
- raise CrosswalkError, "Dor::Item#generate_dublin_core produced incorrect xml (no root):\n#{dc_doc.to_xml}" if dc_doc.root.nil?
22
- raise CrosswalkError, "Dor::Item#generate_dublin_core produced incorrect xml (no children):\n#{dc_doc.to_xml}" if dc_doc.root.children.size == 0
23
-
24
- dc_doc
25
- end
26
-
27
- # @return [String] the DublinCore XML document object
28
- def to_xml
29
- ng_xml.to_xml
30
- end
31
-
32
- private
33
-
34
- def desc_md
35
- return PublicDescMetadataService.new(work).ng_xml(include_access_conditions: false) if include_collection?
36
-
37
- work.descMetadata.ng_xml
38
- end
39
-
40
- def include_collection?
41
- @include_collection
42
- end
43
- attr_reader :work
44
- end
45
- end