caruby-tissue 1.2.1

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 (133) hide show
  1. data/History.txt +4 -0
  2. data/LEGAL +5 -0
  3. data/LICENSE +22 -0
  4. data/README.md +44 -0
  5. data/bin/crtdump +31 -0
  6. data/bin/crtexample +18 -0
  7. data/bin/crtextract +47 -0
  8. data/bin/crtmigrate +17 -0
  9. data/bin/crtsmoke +27 -0
  10. data/examples/galena/README.md +53 -0
  11. data/examples/galena/bin/migrate.rb +42 -0
  12. data/examples/galena/bin/seed.rb +43 -0
  13. data/examples/galena/conf/extract/simple_fields.yaml +4 -0
  14. data/examples/galena/conf/migration/filter_fields.yaml +7 -0
  15. data/examples/galena/conf/migration/filter_migration.yaml +9 -0
  16. data/examples/galena/conf/migration/frozen_fields.yaml +11 -0
  17. data/examples/galena/conf/migration/frozen_migration.yaml +9 -0
  18. data/examples/galena/conf/migration/general_fields.yaml +42 -0
  19. data/examples/galena/conf/migration/general_migration.yaml +9 -0
  20. data/examples/galena/conf/migration/simple_fields.yaml +30 -0
  21. data/examples/galena/conf/migration/simple_migration.yaml +7 -0
  22. data/examples/galena/conf/migration/small_fields.yaml +24 -0
  23. data/examples/galena/conf/migration/small_migration.yaml +9 -0
  24. data/examples/galena/data/filter.csv +1 -0
  25. data/examples/galena/data/frozen.csv +1 -0
  26. data/examples/galena/data/general.csv +1 -0
  27. data/examples/galena/data/minimal.csv +1 -0
  28. data/examples/galena/data/simple.csv +1 -0
  29. data/examples/galena/data/small.csv +1 -0
  30. data/examples/galena/doc/CaTissue.html +93 -0
  31. data/examples/galena/doc/CaTissue/CollectionProtocolRegistration.html +181 -0
  32. data/examples/galena/doc/CaTissue/Participant.html +241 -0
  33. data/examples/galena/doc/CaTissue/SpecimenCollectionGroup.html +190 -0
  34. data/examples/galena/doc/CaTissue/StorageContainer.html +179 -0
  35. data/examples/galena/doc/CaTissue/TissueSpecimen.html +320 -0
  36. data/examples/galena/doc/Galena.html +290 -0
  37. data/examples/galena/doc/Galena/Seed.html +203 -0
  38. data/examples/galena/doc/Galena/Seed/Defaults.html +646 -0
  39. data/examples/galena/doc/_index.html +188 -0
  40. data/examples/galena/doc/class_list.html +36 -0
  41. data/examples/galena/doc/css/common.css +1 -0
  42. data/examples/galena/doc/css/full_list.css +53 -0
  43. data/examples/galena/doc/css/style.css +307 -0
  44. data/examples/galena/doc/file.README.html +108 -0
  45. data/examples/galena/doc/file_list.html +38 -0
  46. data/examples/galena/doc/frames.html +13 -0
  47. data/examples/galena/doc/index.html +108 -0
  48. data/examples/galena/doc/js/app.js +202 -0
  49. data/examples/galena/doc/js/full_list.js +149 -0
  50. data/examples/galena/doc/js/jquery.js +154 -0
  51. data/examples/galena/doc/method_list.html +179 -0
  52. data/examples/galena/doc/top-level-namespace.html +112 -0
  53. data/examples/galena/lib/README.html +33 -0
  54. data/examples/galena/lib/galena.rb +8 -0
  55. data/examples/galena/lib/galena/cli/seed.rb +43 -0
  56. data/examples/galena/lib/galena/migration/filter_shims.rb +43 -0
  57. data/examples/galena/lib/galena/migration/frozen_shims.rb +54 -0
  58. data/examples/galena/lib/galena/seed/defaults.rb +97 -0
  59. data/lib/catissue.rb +26 -0
  60. data/lib/catissue/cli/command.rb +51 -0
  61. data/lib/catissue/cli/example.rb +31 -0
  62. data/lib/catissue/cli/migrate.rb +60 -0
  63. data/lib/catissue/cli/smoke.rb +45 -0
  64. data/lib/catissue/database.rb +451 -0
  65. data/lib/catissue/database/annotation/annotatable_service.rb +25 -0
  66. data/lib/catissue/database/annotation/annotation_service.rb +79 -0
  67. data/lib/catissue/database/annotation/annotator.rb +84 -0
  68. data/lib/catissue/database/annotation/entity_manager.rb +10 -0
  69. data/lib/catissue/database/annotation/integration_service.rb +87 -0
  70. data/lib/catissue/database/controlled_value_finder.rb +43 -0
  71. data/lib/catissue/database/controlled_values.rb +162 -0
  72. data/lib/catissue/domain/abstract_domain_object.rb +8 -0
  73. data/lib/catissue/domain/abstract_position.rb +22 -0
  74. data/lib/catissue/domain/abstract_specimen.rb +288 -0
  75. data/lib/catissue/domain/abstract_specimen_collection_group.rb +25 -0
  76. data/lib/catissue/domain/address.rb +13 -0
  77. data/lib/catissue/domain/cancer_research_group.rb +11 -0
  78. data/lib/catissue/domain/capacity.rb +34 -0
  79. data/lib/catissue/domain/check_in_check_out_event_parameter.rb +19 -0
  80. data/lib/catissue/domain/collection_event_parameters.rb +13 -0
  81. data/lib/catissue/domain/collection_protocol.rb +177 -0
  82. data/lib/catissue/domain/collection_protocol_event.rb +108 -0
  83. data/lib/catissue/domain/collection_protocol_registration.rb +108 -0
  84. data/lib/catissue/domain/consent_tier_response.rb +13 -0
  85. data/lib/catissue/domain/consent_tier_status.rb +29 -0
  86. data/lib/catissue/domain/container.rb +234 -0
  87. data/lib/catissue/domain/container_position.rb +21 -0
  88. data/lib/catissue/domain/container_type.rb +131 -0
  89. data/lib/catissue/domain/department.rb +13 -0
  90. data/lib/catissue/domain/disposal_event_parameters.rb +13 -0
  91. data/lib/catissue/domain/embedded_event_parameters.rb +10 -0
  92. data/lib/catissue/domain/external_identifier.rb +22 -0
  93. data/lib/catissue/domain/frozen_event_parameters.rb +10 -0
  94. data/lib/catissue/domain/institution.rb +13 -0
  95. data/lib/catissue/domain/new_specimen_array_order_item.rb +35 -0
  96. data/lib/catissue/domain/order_details.rb +25 -0
  97. data/lib/catissue/domain/participant.rb +138 -0
  98. data/lib/catissue/domain/participant_medical_identifier.rb +38 -0
  99. data/lib/catissue/domain/password.rb +11 -0
  100. data/lib/catissue/domain/race.rb +11 -0
  101. data/lib/catissue/domain/received_event_parameters.rb +25 -0
  102. data/lib/catissue/domain/scg_event_parameters.rb +11 -0
  103. data/lib/catissue/domain/site.rb +30 -0
  104. data/lib/catissue/domain/specimen.rb +456 -0
  105. data/lib/catissue/domain/specimen_array.rb +47 -0
  106. data/lib/catissue/domain/specimen_array_content.rb +19 -0
  107. data/lib/catissue/domain/specimen_array_type.rb +20 -0
  108. data/lib/catissue/domain/specimen_characteristics.rb +20 -0
  109. data/lib/catissue/domain/specimen_collection_group.rb +412 -0
  110. data/lib/catissue/domain/specimen_event_parameters.rb +111 -0
  111. data/lib/catissue/domain/specimen_position.rb +38 -0
  112. data/lib/catissue/domain/specimen_protocol.rb +34 -0
  113. data/lib/catissue/domain/specimen_requirement.rb +143 -0
  114. data/lib/catissue/domain/storage_container.rb +204 -0
  115. data/lib/catissue/domain/storage_type.rb +82 -0
  116. data/lib/catissue/domain/transfer_event_parameters.rb +53 -0
  117. data/lib/catissue/domain/user.rb +100 -0
  118. data/lib/catissue/extract/command.rb +31 -0
  119. data/lib/catissue/extract/delta.rb +62 -0
  120. data/lib/catissue/extract/extractor.rb +99 -0
  121. data/lib/catissue/migration/migrator.rb +101 -0
  122. data/lib/catissue/migration/shims.rb +108 -0
  123. data/lib/catissue/migration/uniquify.rb +111 -0
  124. data/lib/catissue/resource.rb +84 -0
  125. data/lib/catissue/util/controlled_value.rb +29 -0
  126. data/lib/catissue/util/location.rb +116 -0
  127. data/lib/catissue/util/log.rb +30 -0
  128. data/lib/catissue/util/person.rb +31 -0
  129. data/lib/catissue/util/position.rb +54 -0
  130. data/lib/catissue/util/storable.rb +34 -0
  131. data/lib/catissue/util/storage_type_holder.rb +30 -0
  132. data/lib/catissue/version.rb +7 -0
  133. metadata +212 -0
@@ -0,0 +1,31 @@
1
+ require 'catissue/cli/command'
2
+ require 'caruby/util/collection'
3
+
4
+ module CaTissue
5
+ module CLI
6
+ class Example < Command
7
+ def initialize
8
+ super(SPECS) { |opts| list }
9
+ end
10
+
11
+ private
12
+
13
+ SPECS = [
14
+ [:list, "-l", "--list", "Prints the example operations and exits"]
15
+ ]
16
+
17
+ # Lists the examples.
18
+ def list
19
+ root = CaTissue.path('examples')
20
+ Dir.foreach(root) do |f|
21
+ path = File.expand_path(f, root)
22
+ if File.directory?(path) and f[0, 1] != '.' then
23
+ readme = File.join(path, 'doc', 'index.html')
24
+ citation = "(see doc/index.html)" if File.readable?(readme)
25
+ puts "#{f} - #{path} #{citation}"
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ # the default log file
2
+ DEF_LOG_FILE = 'log/migration.log'
3
+
4
+ require 'catissue/cli/command'
5
+ require 'catissue/migration/migrator'
6
+
7
+ module CaTissue
8
+ module CLI
9
+ class Migrate < Command
10
+ # The migration option specifications.
11
+ #
12
+ # The :unique option ensures that the migrated objects do not conflict with existing or future
13
+ # objects. This is used for testing a migration dry run. It is recommended that the trial run
14
+ # protocol is set to a test protocol as well.
15
+ SPECS = [
16
+ [:input, "input", "Source file to migrate"],
17
+ [:target, "-t", "--target CLASS", "Migration target class"],
18
+ [:mapping, "-m", "--mapping FILE", "The input field => caTissue attribute mapping file"],
19
+ [:shims, "-s", "--shims FILE[,FILE...]", Array, "Migration customization shim files to load"],
20
+ [:bad, "-b", "--bad FILE", "Write each invalid record to the given file and continue migration"],
21
+ [:unique, "-u", "--unique", "Make the migrated objects unique for testing"],
22
+ [:offset, "--offset N", Integer, "Number of input records to skip before starting the migration"]
23
+ ]
24
+
25
+ # Creates a {CaTissue::CLI::Migrate} command with the given standard command line specifications
26
+ # as well as the {SPECS} command line specifications.
27
+ #
28
+ # @param (see CaRuby::CLI::Command#initialize)
29
+ def initialize(specs={}, &factory)
30
+ super(specs.merge(SPECS)) { |opts| migrate(opts, &factory) }
31
+ end
32
+
33
+ # Starts a Migrator with the command-line options.
34
+ #
35
+ # @yield [target] operation on the migration target
36
+ # @yieldparam [CaRuby::Resource] the migrated domain object
37
+ # @see CaRuby::Command#run
38
+ def migrate(opts, &factory)
39
+ super do |opts|
40
+ validate(opts)
41
+ migrator = block_given ? yield(opts) : Migrator.new(opts)
42
+ migrator.migrate
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def validate(opts)
49
+ tgt = opts[:target]
50
+ if tgt.nil? then raise ArgumentError.new("Missing required migration target class option") end
51
+ begin
52
+ opts[:target] = CaTissue.const_get(tgt)
53
+ rescue Exception
54
+ logger.fatal("Could not load CaTissue class #{tgt} - #{$!}.\n#{$@.qp}")
55
+ raise MigrationError.new("Could not load migration target class #{tgt}")
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,45 @@
1
+ require 'catissue/cli/command'
2
+ require 'catissue/database'
3
+
4
+ module CaTissue
5
+ module CLI
6
+ class Smoke < Command
7
+ # Creates a new Smoke command.
8
+ def initialize
9
+ super { |opts| execute(opts) }
10
+ end
11
+
12
+ private
13
+
14
+ DB_MSG = "Verifying database access by searching for the pre-defined 'In Transit' Site..."
15
+
16
+ # Runs the smoke test.
17
+ def execute
18
+ puts DB_MSG
19
+ logger.info(DB_MSG)
20
+ # connect to the database and query on a Site
21
+ CaTissue::Database.instance.open { find_in_transit_site }
22
+ end
23
+
24
+ def find_in_transit_site
25
+ begin
26
+ site = CaTissue::Site.new(:name => 'In Transit').find
27
+ rescue Exception => e
28
+ logger.error("caTissue database access was unsuccessful - #{e}:\n#{e.backtrace.qp}")
29
+ puts "caTissue database access was unsuccessful - #{e}."
30
+ puts "See the log at #{DEF_LOG_FILE} for more information."
31
+ end
32
+
33
+ if site then
34
+ puts "The 'In Transit' Site was found with identifier #{site.identifier}."
35
+ puts "Smoke test successful."
36
+ exit 0
37
+ else
38
+ puts "The 'In Transit' Site was not found."
39
+ puts "Smoke test unsuccessful."
40
+ exit 69 # service unavailable error status
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,451 @@
1
+ require 'singleton'
2
+ require 'caruby/util/topological_sync_enumerator'
3
+ require 'caruby/database'
4
+ # TODO - enable DE annotator
5
+ #require 'catissue/database/annotation/annotator'
6
+ require 'catissue/domain/scg_event_parameters'
7
+
8
+ module CaTissue
9
+ # A CaTissue::Database mediates access to the caTissue database.
10
+ # The CaRuby::Database functionality is preserved and not expanded, but this CaTissue::Database overrides
11
+ # several base class private methods to enable alternate CaTissue-specific search strategies and work
12
+ # around caTissue and caCORE bugs.
13
+ class Database < CaRuby::Database
14
+ include Singleton
15
+
16
+ # The application service name
17
+ SERVICE_NAME = 'catissuecore'
18
+
19
+ # The default database name
20
+ DEF_DATABASE_NAME = 'catissue'
21
+
22
+ # Creates a new Database with the {SERVICE_NAME} service and {CaTissue.access_properties}.
23
+ def initialize
24
+ super(SERVICE_NAME, CaTissue.access_properties)
25
+ end
26
+
27
+ private
28
+
29
+ def specimen_compatible?(target, source)
30
+ target.class === source and
31
+ specimen_parent_compatible?(target, source) and
32
+ (target.specimen_type == source.specimen_type or source.specimen_type == 'Not Specified') and
33
+ (target.pathological_status == source.pathological_status or source.pathological_status == 'Not Specified')
34
+ end
35
+
36
+ def specimen_parent_compatible?(target, source)
37
+ if target.parent then
38
+ source.parent and source.parent.identifier == target.parent.identifier
39
+ else
40
+ source.parent.nil?
41
+ end
42
+ end
43
+
44
+ # This method patches up fetched sources to correct the following anomaly:
45
+ #
46
+ # caCORE alert - fetched references are not reconciled within an existing query result, e.g.
47
+ # given a query result with two Specimens s1 and s2, the parent reference is not fetched.
48
+ # Subsequently fetching the parent is independent of the query result. Thus if s1 is the parent
49
+ # of s2 in the database, the fetched s2 parent s3 is distinct from s1, even though
50
+ # s1.identifier == s3.identifier. Thus, enforcing reference consistency requires a post-fetch step
51
+ # that matches the fetched objects to the original query result on identifier and resets the
52
+ # references.
53
+ def resolve_parent(refs, attribute)
54
+ id_ref_hash = refs.to_compact_hash { |ref| ref.identifier }.invert
55
+ refs.each do |ref|
56
+ parent = ref.send(attribute) || next
57
+ resolved = id_ref_hash[parent.identifier] || next
58
+ logger.debug { "Resetting #{ref.qp} #{attribute} from #{parent} to #{resolved} in order to fix a caCORE inconsistency..." }
59
+ ref.set_attribute(attribute, resolved)
60
+ end
61
+ end
62
+
63
+ # Work around the following caTissue bugs:
64
+ # * caTissue alert - Bug #135: Update SCG SpecimenEventParameters raises AuditException.
65
+ # Work around is to update the SCG instead.
66
+ #
67
+ # @param (see CaRuby::Database#update_object)
68
+ # @return (see CaRuby::Database#update_object)
69
+ def update_object(obj)
70
+ if scg_event_parameters?(obj) then
71
+ update_scg_event_parameters(obj)
72
+ else
73
+ if CaTissue::Specimen === obj
74
+ # Specimen activity status is not always set to default; don't know why.
75
+ # TODO - isolate and fix at source
76
+ obj.activity_status ||= 'Active'
77
+ end
78
+ super
79
+ end
80
+ end
81
+
82
+ # Overrides #{CaRuby::Database::Writer#recursive_save?} to support the update work-around
83
+ # described in {#update_object}. A recursive SCG update is allowed if the nested
84
+ # transaction sequence is:
85
+ # * Update SCG
86
+ # * Update SCG event parameters as part of Update SCG
87
+ # * Update SCG as part of the Bug #135 work-around
88
+ #
89
+ # @param (see CaRuby::Database::Writer#recursive_save?)
90
+ # @return (see CaRuby::Database::Writer#recursive_save?)
91
+ def recursive_save?(obj, operation)
92
+ super and not scg_event_update_workaround?(obj, operation)
93
+ end
94
+
95
+ # Returns whether operation is the second SCG Update described in {#recursive_save?}.
96
+ def scg_event_update_workaround?(obj, operation)
97
+ return false unless CaTissue::SpecimenCollectionGroup == obj and operation == :update
98
+ last = @operations.last
99
+ return false unless last and scg_event_parameters?(last.subject)
100
+ ev = last.subject
101
+ return false unless ev.specimen_collection_group == obj
102
+ penultimate = @operations[-2]
103
+ penultimate and penultimate.subject == obj
104
+ end
105
+
106
+ # Augments {CaRuby::Database#save_with_template} to work around the following caTissue anomalies:
107
+ #
108
+ # caTissue alert - Bug #149: API update TissueSpecimen position validation incorrect.
109
+ # The Specimen update argument must reference the old position, even though the position is not
110
+ # updatable, unless old status is Pending. The validation defect described in Bug #149 requires
111
+ # a work-around that is also used for a different reason described in the following paragraph.
112
+ #
113
+ # caTissue alert - Update of a {CaTissue::Specimen} which references a position must include the former
114
+ # position in the caTissue service update argument. A Specimen position is altered as a side-effect
115
+ # by creating a proxy save {CaTissue::TransferEventParameters}. The changed position is not reflected
116
+ # in the Specimen position, which must be refetched to reflect the database state. This fetch is
117
+ # done automatically by {CaRuby::Database} as part of the save proxy mechanism. The Specimen update
118
+ # template must include a reference to the former position but not the changed position.
119
+ #
120
+ # However, the Specimen {CaRuby::Writer#update} argument will include the changed position, not the
121
+ # former position. The template built {CaRuby::Writer#update} for submission to the caTissue app
122
+ # does not include a position reference, since the position has a save proxy which handles position
123
+ # change as part of the {CaRuby::Writer} update dependent propagation.
124
+ #
125
+ # Thus, updating a Specimen which includes a position change is performed as follows:
126
+ # * reconstitute the former position from the Position snapshot taken as part of the
127
+ # {CaRuby::Persistable} change tracker.
128
+ # * add the former position to the template (which will now differ from the {CaRuby::Writer#update}
129
+ # argument).
130
+ # * submit the adjusted Specimen template to the caTissue app updateObject.
131
+ # * {CaRuby::Writer#update} will propagate the Specimen update to the changed position dependent,
132
+ # which in turn saves via the {CaTissue::TransferEventParameters} proxy.
133
+ # * The proxy save will in turn refetch the proxied Specimen position to obtain the identifier
134
+ # and merge this into the Specimen position.
135
+ # * The Specimen update template is used solely to satisfy the often arcane caTissue interaction
136
+ # requirements like this work-around, and is thrown away along with its aberrant state.
137
+ #
138
+ # This work-around is the only case of a save template modification to handle a caTissue special
139
+ # case. Note that the {CaTissue::SpecimenPosition} logic does not apply to a
140
+ # {CaTissue::ContainerPosition}, which can be updated directly.
141
+ #
142
+ # The additional complexity of this work-around is necessitated by the caTissue policy of update
143
+ # by indirect server-side side-effects that are not reflected back to the client. The caRuby
144
+ # policy of a declarative API that persists the save argument as given and reflects the
145
+ # changed database state requires this work-around.
146
+ #
147
+ # @param obj (see #store)
148
+ # @param [Resource] template the obj template to submit to caCORE
149
+ def save_with_template(obj, template)
150
+ if CaTissue::Specimen === obj and obj.position and obj.position.identifier then
151
+ add_position_to_specimen_template(obj, template)
152
+ end
153
+ super
154
+ end
155
+
156
+ # Adds the specimen position to its save template.
157
+ #
158
+ # @param [CaTissue::Specimen] specimen the existing specimen with an existing position
159
+ # @param template (see #save_with_template)
160
+ # @see {#save_with_template}
161
+ def add_position_to_specimen_template(specimen, template)
162
+ pos = specimen.position
163
+ # the non-domain position attributes
164
+ attrs = pos.class.nondomain_attributes
165
+ # the template position reflects the old values, if available
166
+ ss = pos.snapshot
167
+ # the attribute => value hash
168
+ vh = ss ? attrs.to_compact_hash { |attr| ss[attr] } : pos.value_hash(attrs)
169
+ vh[:specimen] = template
170
+ vh[:storage_container] = pos.storage_container.copy
171
+ # the template position reflects the old values
172
+ template.position = pos.class.new(vh)
173
+ logger.debug { "Work around #{specimen} update anomaly by copying position #{template.position.qp} to update template #{template.qp} as #{template.position.qp} with values #{vh.qp}..." }
174
+ end
175
+
176
+ # @return [Boolean] whether obj is a SCGEventParameters with a SCG owner
177
+ def scg_event_parameters?(obj)
178
+ SCGEventParameters === obj and obj.specimen_collection_group
179
+ end
180
+
181
+ # @param [SCGEventParameters] ep the SCG event parameters to update
182
+ # @return (see CaRuby::Database#update_object)
183
+ def update_scg_event_parameters(ep)
184
+ scg = ep.specimen_collection_group
185
+ logger.debug { "Work around #{ep.qp} caTissue SCG SpecimenEventParameters update bug by updating the owner #{scg.qp} instead..." }
186
+ ensure_exists(scg)
187
+ # update the SCGEventParameters by updating the SCG
188
+ update(scg)
189
+ raise CaRuby::DatabaseError.new("Update SCG did not cascade to dependent #{ep}") unless ep.identifier
190
+ ep
191
+ end
192
+
193
+ # Overrides {CaRuby::Database::Writer#save_dependents} to handle the work-around described
194
+ # in {#save_specimen_dependents}.
195
+ #
196
+ # @param (see CaRuby::Writer#save_dependents)
197
+ def save_dependents(obj)
198
+ Specimen === obj ? save_specimen_dependents(obj) { super } : super
199
+ end
200
+
201
+ # Overrides {CaRuby::Database::Writer#save_dependents} on a Specimen to correct the
202
+ # following problem:
203
+ #
204
+ # caTissue alert - DisposalEventParameters must be created after all other Specimen SEPs.
205
+ #
206
+ # The process for migrating a discarded Specimen is as follows:
207
+ # * Create the Specimen with status Active.
208
+ # * Create the non-disposal events.
209
+ # * Create the DisposalEventParameters.
210
+ #
211
+ # A DisposalEventParameters cannot be created for a closed Specimen. Conversely, caTissue closes
212
+ # the Specimen as a side-effect of creating a DisposalEventParameters. Therefore, even if the
213
+ # client submits a closed Specimen for create, this CaTissue::Database must first create the
214
+ # Specimen with status Active, then submit the DisposalEventParameters.
215
+ #
216
+ # This is a work-around on top of the {#create_unavailable_specimen} work-around. See that method
217
+ # for the subtle interaction required between these two work-arounds.
218
+ #
219
+ # @param [CaTissue::Specimen] the specimen whose dependents are to be saved
220
+ # @yield [dependent] calls the base {CaRuby::Writer#save_dependents}
221
+ # @yieldparam [Resource] dependent the dependent to save
222
+ def save_specimen_dependents(specimen)
223
+ dsp = specimen.specimen_events.detect { |ep| CaTissue::DisposalEventParameters === ep }
224
+ if dsp then
225
+ logger.debug { "Work around caTissue #{specimen.qp} event parameters save order dependency by deferring #{dsp.qp} save..." }
226
+ specimen.specimen_events.delete(dsp)
227
+ end
228
+
229
+ begin
230
+ yield specimen
231
+ ensure
232
+ specimen.specimen_events << dsp if dsp
233
+ end
234
+
235
+ # save the deferred disposal if any
236
+ if dsp then
237
+ logger.debug { "Creating deferred #{specimen.qp} dependent #{dsp.qp}..." }
238
+ save_dependent(dsp)
239
+ end
240
+ end
241
+
242
+ # Overrides {CaRuby::Database::Writer#save_with_template} to work around the following
243
+ # caTissue bugs:
244
+ # * caTissue alert - Bug TODO: CollectionProtocolRegistration must cascade through
245
+ # CP, but the CP events cannot cascade to SpecimenRequirement without raising an
246
+ # Exception. Work-around is to clear the template CP events.
247
+ # * caTissue alert - Bug #164: Update Specimen with unchanged ExternalIdentifier fails.
248
+ # Work-around is to clear the update template external_identifiers.
249
+ def save_with_template(obj, template)
250
+ if CaTissue::CollectionProtocolRegistration === obj and template.collection_protocol then
251
+ template.collection_protocol.collection_protocol_events.clear
252
+ elsif obj.identifier and CaTissue::Specimen === obj
253
+ sv_eids = obj.external_identifiers.select { |eid| eid.changed? }
254
+ unless sv_eids.empty? then
255
+ logger.debug { "Work around caTissue Bug #164 by updating the #{obj.qp} changed external_identifiers separately: #{sv_eids.qp}." }
256
+ sv_eids.each { |eid| eid.identifier ? update(eid) : create(eid) }
257
+ end
258
+ logger.debug { "Work around caTissue Bug #164 by setting the #{obj.qp} update template #{template.qp} external_identifiers to nil." }
259
+ template.external_identifiers = nil
260
+ end
261
+ super
262
+ end
263
+
264
+ # Augment {CaRuby::Database::Writer#create_object} for the following work-arounds:
265
+ # * caTissue alert - Bug #124: SpecimenEventParameters with SCG rather than Specimen fails validation.
266
+ # Work-around is to create the SEP with the specimen set to a SCG specimen, then unset the
267
+ # specimen reference.
268
+ # * if obj is a CaTissue::Specimen with the is_available flag set to false, then work around the bug
269
+ # described in {#create_unavailable_specimen}.
270
+ # * caTissue alert - Bug #161: Specimen API disposal not reflected in result activity status.
271
+ # DisposalEventParameters create sets the owner Specimen activity_status to +Closed+ as a side-effect.
272
+ # Reflect this side-effect in the submitted DisposalEventParameters owner Specimen object.
273
+ def create_object(obj)
274
+ # if CaTissue::SCGEventParameters === obj and obj.specimen_collection_group then
275
+ # return create_scg_event_parameters(obj) { super }
276
+ # elsif CaTissue::Specimen === obj and obj.is_available == false then
277
+ if CaTissue::Specimen === obj and not obj.available? then
278
+ # Note that the obj.is_available == false test is required as opposed to obj.is_available?,
279
+ # since a nil is_available flag does not imply an unavailable specimen.
280
+ return create_unavailable_specimen(obj) { super }
281
+ end
282
+
283
+ # standard create
284
+ super
285
+
286
+ # replicate caTissue create side-effects in the submitted object
287
+ if CaTissue::DisposalEventParameters === obj then
288
+ obj.specimen.activity_status = 'Closed'
289
+ logger.debug { "Set the created DisposalEventParameters #{obj.qp} owner #{obj.specimen.qp} activity status to Closed." }
290
+ end
291
+
292
+ obj
293
+ end
294
+
295
+ # Creates the given specimen by working around the following bug:
296
+ #
297
+ # caTissue alert - Bug #160: Missing Is Available? validation.
298
+ # Cannot create a Specimen with any of the following conditions:
299
+ # * zero available_quantity
300
+ # * is_available flag set to false
301
+ # * activity_status is +Closed+
302
+ #
303
+ # The work-around is to set the flags to true and +Active+, resp., set the quantities
304
+ # to a non-zero value, create the Specimen and then update the created Specimen with
305
+ # the original values.
306
+ #
307
+ # If spc has a disposal event, then this work-around interacts with the {#save_dependents}
308
+ # work-around as follows:
309
+ # * delete that event from the Specimen.
310
+ # * Create the Specimen as described above.
311
+ # * Update the Specimen as described above, but do no set the activity_status to +Closed+.
312
+ # * Create the pending disposal event.
313
+ #
314
+ # @param [CaTissue::Specimen] specimen the specimen to create
315
+ def create_unavailable_specimen(specimen)
316
+ logger.debug { "Resetting #{specimen} quantities and available flag temporarily to work around caTissue Bug #160..." }
317
+ specimen.is_available = true
318
+ oiqty = specimen.initial_quantity
319
+ oaqty = specimen.available_quantity
320
+ ostatus = specimen.activity_status
321
+ specimen.initial_quantity = 1.0
322
+ specimen.available_quantity = 1.0
323
+ specimen.activity_status = 'Active'
324
+ # Cannot reset a disposed Specimen quantity, so postpone disposal until
325
+ # quantities are reset.
326
+ dsp = specimen.specimen_events.detect { |sep| CaTissue::DisposalEventParameters === sep }
327
+ if dsp then
328
+ specimen.specimen_events.delete(dsp)
329
+ end
330
+
331
+ # delegate to standard create
332
+ yield
333
+
334
+ logger.debug { "Reupdating created #{specimen} with initial quantity and available flag set back to original values to complete caTissue Bug #160 work-around..." }
335
+ specimen.is_available = false
336
+ specimen.initial_quantity = oiqty
337
+ # the available quantity is always zero, since the available flag is set to false
338
+ specimen.available_quantity = 0.0
339
+ # Leave status Active if there is a disposal event, since quantities cannot be reset
340
+ # on a closed Specimen and creating the disposal event below will close the Specimen.
341
+ specimen.activity_status = ostatus unless dsp
342
+ update(specimen)
343
+
344
+ # Finally, create the disposal event if one is pending.
345
+ if dsp then
346
+ specimen.specimen_events << dsp
347
+ create(dsp)
348
+ end
349
+
350
+ logger.debug { "#{specimen} caTissue Bug #160 work-around completed." }
351
+ specimen
352
+ end
353
+
354
+ def fetch_object(obj)
355
+ super or fetch_alternative(obj)
356
+ end
357
+
358
+ def fetch_alternative(obj)
359
+ case obj
360
+ when CaTissue::Specimen then
361
+ fetch_specimen_alternative(obj)
362
+ when CaTissue::Participant then
363
+ fetch_participant_alternative(obj)
364
+ end
365
+ end
366
+
367
+ # Override {CaRuby::Database#query_safe} to work around the following +caTissue+ bugs:
368
+ # * caTissue alert - Specimen auto-generates blank ExternalIdentifier.
369
+ # cf. https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=436&sid=ef98f502fc0ab242781b7759a0eaff36
370
+ # * caTissue alert - Specimen auto-generates blank PMI.
371
+ def query_safe(obj_or_hql, *path)
372
+ if path.last == :external_identifiers then
373
+ CaTissue::Specimen.remove_empty_external_identifier(super)
374
+ elsif path.last == :participant_medical_identifiers then
375
+ CaTissue::Specimen.remove_empty_medical_identifier(super)
376
+ else
377
+ super
378
+ end
379
+ end
380
+
381
+ # caTissue alert - Bug #147: SpecimenRequirement query ignores CPE.
382
+ # Work around this bug by an inverted query on the referenced CPE.
383
+ def query_object(obj, attribute=nil)
384
+ if CaTissue::SpecimenRequirement === obj and not obj.identifier and obj.collection_protocol_event then
385
+ query_requirement_using_cpe_inversion(obj, attribute)
386
+ else
387
+ super
388
+ end
389
+ end
390
+
391
+ # caCORE alert - Override {CaRuby::Database::Reader#invertible_query?} to enable the Bug #147 work
392
+ # around in {#query_object}. Invertible queries are performed to work around Bug #79. However, this
393
+ # work-around induces Bug #147, so we disable the Bug #79 work-around here for the special case of
394
+ # a CPE in order to enable the Bug #147 work-around. And so it goes....
395
+ def invertible_query?(obj, attribute)
396
+ super and not (CaTissue::CollectionProtocolEvent === obj and attribute == :specimen_requirements)
397
+ end
398
+
399
+ # Returns the Annotation::Annotator service for klass, or nil if klass is neither annotatable nor an annotation.
400
+ def annotation_service(klass)
401
+ # create the single annotator on demand
402
+ @annotator ||= CaTissue::Annotation::Annotator.new(self)
403
+ # find the service or return nil if kla
404
+ @annotator.service(klass)
405
+ end
406
+
407
+ def fetch_participant_alternative(pnt)
408
+ fetch_participant_using_ppi(pnt) or fetch_participant_using_mrn(pnt)
409
+ end
410
+
411
+ def fetch_participant_using_ppi(pnt)
412
+ cpr = pnt.registrations.first
413
+ return if cpr.nil?
414
+ logger.debug { "Using alternative Participant fetch strategy to find Participant by protocol participant identifier..." }
415
+ return unless exists?(cpr)
416
+ candidates = query(cpr.copy, :participant)
417
+ candidates.first if candidates.size == 1
418
+ end
419
+
420
+ def fetch_participant_using_mrn(pnt)
421
+ mid = pnt.medical_identifiers.first
422
+ return if mid.nil?
423
+ logger.debug { "Using alternative Participant fetch strategy to find Participant by medical record number..." }
424
+ return unless exists?(mid)
425
+ candidates = query(mid.copy, :participant)
426
+ candidates.first if candidates.size == 1
427
+ end
428
+
429
+ # Returns a fetched Specimen which matches spc on at least one external identifier, or nil if no match.
430
+ def fetch_specimen_alternative(spc)
431
+ eid = spc.external_identifiers.detect { |eid| eid.identifier } || spc.external_identifiers.first || return
432
+ logger.debug { "Using alternative Specimen fetch strategy to find #{spc} by external identifier #{eid}..." }
433
+ candidates = query(eid.copy, :specimen)
434
+ candidates.first if candidates.size == 1
435
+ end
436
+
437
+ def query_scg_specimens_using_specimen_reference(scg)
438
+ spc = CaTissue::Specimen.new(:specimen_collection_group => scg.copy)
439
+ logger.debug { "Using alternative SCG specimens query strategy to find Specimens by Specimen SCG reference #{scg.qp}..." }
440
+ query(spc)
441
+ end
442
+
443
+ def query_requirement_using_cpe_inversion(rqmt, attribute)
444
+ cpe = rqmt.collection_protocol_event
445
+ logger.debug { "Using alternative SpecimenRequirement query strategy to find SpecimenRequirements by inverted CPE reference #{cpe}..." }
446
+ path = [:specimen_requirements]
447
+ path << attribute if attribute
448
+ query(cpe, *path)
449
+ end
450
+ end
451
+ end