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.
- data/History.txt +4 -0
- data/LEGAL +5 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/bin/crtdump +31 -0
- data/bin/crtexample +18 -0
- data/bin/crtextract +47 -0
- data/bin/crtmigrate +17 -0
- data/bin/crtsmoke +27 -0
- data/examples/galena/README.md +53 -0
- data/examples/galena/bin/migrate.rb +42 -0
- data/examples/galena/bin/seed.rb +43 -0
- data/examples/galena/conf/extract/simple_fields.yaml +4 -0
- data/examples/galena/conf/migration/filter_fields.yaml +7 -0
- data/examples/galena/conf/migration/filter_migration.yaml +9 -0
- data/examples/galena/conf/migration/frozen_fields.yaml +11 -0
- data/examples/galena/conf/migration/frozen_migration.yaml +9 -0
- data/examples/galena/conf/migration/general_fields.yaml +42 -0
- data/examples/galena/conf/migration/general_migration.yaml +9 -0
- data/examples/galena/conf/migration/simple_fields.yaml +30 -0
- data/examples/galena/conf/migration/simple_migration.yaml +7 -0
- data/examples/galena/conf/migration/small_fields.yaml +24 -0
- data/examples/galena/conf/migration/small_migration.yaml +9 -0
- data/examples/galena/data/filter.csv +1 -0
- data/examples/galena/data/frozen.csv +1 -0
- data/examples/galena/data/general.csv +1 -0
- data/examples/galena/data/minimal.csv +1 -0
- data/examples/galena/data/simple.csv +1 -0
- data/examples/galena/data/small.csv +1 -0
- data/examples/galena/doc/CaTissue.html +93 -0
- data/examples/galena/doc/CaTissue/CollectionProtocolRegistration.html +181 -0
- data/examples/galena/doc/CaTissue/Participant.html +241 -0
- data/examples/galena/doc/CaTissue/SpecimenCollectionGroup.html +190 -0
- data/examples/galena/doc/CaTissue/StorageContainer.html +179 -0
- data/examples/galena/doc/CaTissue/TissueSpecimen.html +320 -0
- data/examples/galena/doc/Galena.html +290 -0
- data/examples/galena/doc/Galena/Seed.html +203 -0
- data/examples/galena/doc/Galena/Seed/Defaults.html +646 -0
- data/examples/galena/doc/_index.html +188 -0
- data/examples/galena/doc/class_list.html +36 -0
- data/examples/galena/doc/css/common.css +1 -0
- data/examples/galena/doc/css/full_list.css +53 -0
- data/examples/galena/doc/css/style.css +307 -0
- data/examples/galena/doc/file.README.html +108 -0
- data/examples/galena/doc/file_list.html +38 -0
- data/examples/galena/doc/frames.html +13 -0
- data/examples/galena/doc/index.html +108 -0
- data/examples/galena/doc/js/app.js +202 -0
- data/examples/galena/doc/js/full_list.js +149 -0
- data/examples/galena/doc/js/jquery.js +154 -0
- data/examples/galena/doc/method_list.html +179 -0
- data/examples/galena/doc/top-level-namespace.html +112 -0
- data/examples/galena/lib/README.html +33 -0
- data/examples/galena/lib/galena.rb +8 -0
- data/examples/galena/lib/galena/cli/seed.rb +43 -0
- data/examples/galena/lib/galena/migration/filter_shims.rb +43 -0
- data/examples/galena/lib/galena/migration/frozen_shims.rb +54 -0
- data/examples/galena/lib/galena/seed/defaults.rb +97 -0
- data/lib/catissue.rb +26 -0
- data/lib/catissue/cli/command.rb +51 -0
- data/lib/catissue/cli/example.rb +31 -0
- data/lib/catissue/cli/migrate.rb +60 -0
- data/lib/catissue/cli/smoke.rb +45 -0
- data/lib/catissue/database.rb +451 -0
- data/lib/catissue/database/annotation/annotatable_service.rb +25 -0
- data/lib/catissue/database/annotation/annotation_service.rb +79 -0
- data/lib/catissue/database/annotation/annotator.rb +84 -0
- data/lib/catissue/database/annotation/entity_manager.rb +10 -0
- data/lib/catissue/database/annotation/integration_service.rb +87 -0
- data/lib/catissue/database/controlled_value_finder.rb +43 -0
- data/lib/catissue/database/controlled_values.rb +162 -0
- data/lib/catissue/domain/abstract_domain_object.rb +8 -0
- data/lib/catissue/domain/abstract_position.rb +22 -0
- data/lib/catissue/domain/abstract_specimen.rb +288 -0
- data/lib/catissue/domain/abstract_specimen_collection_group.rb +25 -0
- data/lib/catissue/domain/address.rb +13 -0
- data/lib/catissue/domain/cancer_research_group.rb +11 -0
- data/lib/catissue/domain/capacity.rb +34 -0
- data/lib/catissue/domain/check_in_check_out_event_parameter.rb +19 -0
- data/lib/catissue/domain/collection_event_parameters.rb +13 -0
- data/lib/catissue/domain/collection_protocol.rb +177 -0
- data/lib/catissue/domain/collection_protocol_event.rb +108 -0
- data/lib/catissue/domain/collection_protocol_registration.rb +108 -0
- data/lib/catissue/domain/consent_tier_response.rb +13 -0
- data/lib/catissue/domain/consent_tier_status.rb +29 -0
- data/lib/catissue/domain/container.rb +234 -0
- data/lib/catissue/domain/container_position.rb +21 -0
- data/lib/catissue/domain/container_type.rb +131 -0
- data/lib/catissue/domain/department.rb +13 -0
- data/lib/catissue/domain/disposal_event_parameters.rb +13 -0
- data/lib/catissue/domain/embedded_event_parameters.rb +10 -0
- data/lib/catissue/domain/external_identifier.rb +22 -0
- data/lib/catissue/domain/frozen_event_parameters.rb +10 -0
- data/lib/catissue/domain/institution.rb +13 -0
- data/lib/catissue/domain/new_specimen_array_order_item.rb +35 -0
- data/lib/catissue/domain/order_details.rb +25 -0
- data/lib/catissue/domain/participant.rb +138 -0
- data/lib/catissue/domain/participant_medical_identifier.rb +38 -0
- data/lib/catissue/domain/password.rb +11 -0
- data/lib/catissue/domain/race.rb +11 -0
- data/lib/catissue/domain/received_event_parameters.rb +25 -0
- data/lib/catissue/domain/scg_event_parameters.rb +11 -0
- data/lib/catissue/domain/site.rb +30 -0
- data/lib/catissue/domain/specimen.rb +456 -0
- data/lib/catissue/domain/specimen_array.rb +47 -0
- data/lib/catissue/domain/specimen_array_content.rb +19 -0
- data/lib/catissue/domain/specimen_array_type.rb +20 -0
- data/lib/catissue/domain/specimen_characteristics.rb +20 -0
- data/lib/catissue/domain/specimen_collection_group.rb +412 -0
- data/lib/catissue/domain/specimen_event_parameters.rb +111 -0
- data/lib/catissue/domain/specimen_position.rb +38 -0
- data/lib/catissue/domain/specimen_protocol.rb +34 -0
- data/lib/catissue/domain/specimen_requirement.rb +143 -0
- data/lib/catissue/domain/storage_container.rb +204 -0
- data/lib/catissue/domain/storage_type.rb +82 -0
- data/lib/catissue/domain/transfer_event_parameters.rb +53 -0
- data/lib/catissue/domain/user.rb +100 -0
- data/lib/catissue/extract/command.rb +31 -0
- data/lib/catissue/extract/delta.rb +62 -0
- data/lib/catissue/extract/extractor.rb +99 -0
- data/lib/catissue/migration/migrator.rb +101 -0
- data/lib/catissue/migration/shims.rb +108 -0
- data/lib/catissue/migration/uniquify.rb +111 -0
- data/lib/catissue/resource.rb +84 -0
- data/lib/catissue/util/controlled_value.rb +29 -0
- data/lib/catissue/util/location.rb +116 -0
- data/lib/catissue/util/log.rb +30 -0
- data/lib/catissue/util/person.rb +31 -0
- data/lib/catissue/util/position.rb +54 -0
- data/lib/catissue/util/storable.rb +34 -0
- data/lib/catissue/util/storage_type_holder.rb +30 -0
- data/lib/catissue/version.rb +7 -0
- 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
|