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,38 @@
1
+
2
+
3
+ module CaTissue
4
+ # import the Java class
5
+ java_import('edu.wustl.catissuecore.domain.ParticipantMedicalIdentifier')
6
+
7
+
8
+ # The ParticipantMedicalIdentifier domain class.
9
+ class ParticipantMedicalIdentifier
10
+ include Resource
11
+
12
+ # Sets this ParticipantMedicalIdentifier's medical record number to the given value.
13
+ # A Numeric value is converted to a String.
14
+ def medical_record_number=(value)
15
+ value = value.to_s if value
16
+ setMedicalRecordNumber(value)
17
+ end
18
+
19
+ set_secondary_key_attributes(:site, :medical_record_number)
20
+
21
+ add_mandatory_attributes(:participant)
22
+
23
+ private
24
+
25
+ # Adds defaults as follows:
26
+ # * The default site is the particiant registration protocol site, if unique.
27
+ def add_defaults_local
28
+ super
29
+ self.site ||= default_site
30
+ end
31
+
32
+ def default_site
33
+ cprs = participant.registrations if participant
34
+ cp = cprs.first.protocol if cprs and cprs.size == 1
35
+ cp.sites.first if cp and cp.sites.size == 1
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ module CaTissue
2
+ # import the Java class
3
+ java_import('edu.wustl.catissuecore.domain.Password')
4
+
5
+ class Password
6
+ include Resource
7
+
8
+ qualify_attribute(:update_date, :unsaved)
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module CaTissue
2
+ # import the Java class
3
+ java_import('edu.wustl.catissuecore.domain.Race')
4
+
5
+ class Race
6
+ include Resource
7
+
8
+ add_attribute_defaults(:race_name => 'Unknown')
9
+
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ require 'catissue/domain/scg_event_parameters'
2
+
3
+ module CaTissue
4
+ # import the Java class
5
+ java_import('edu.wustl.catissuecore.domain.ReceivedEventParameters')
6
+
7
+ class ReceivedEventParameters
8
+ include Resource, SCGEventParameters
9
+
10
+ add_attribute_aliases(:receiver => :user)
11
+
12
+ add_attribute_defaults(:received_quality => 'Not Specified')
13
+
14
+ add_mandatory_attributes(:received_quality)
15
+
16
+ private
17
+
18
+ # Returns the first SCG CP coordinator, if any.
19
+ def default_user
20
+ scg = specimen_collection_group || (specimen.specimen_collection_group if specimen) || return
21
+ cp = scg.collection_protocol || return
22
+ cp.coordinators.first || (cp.sites.first.coordinator if cp.sites.size === 1)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module CaTissue
2
+ # A SCGEventParameters is a SpecimenEventParameters which can be owned by a SpecimenCollectionGroup.
3
+ module SCGEventParameters
4
+ # Returns the SpecimenEventParameters in others which matches this SCGEventParameters in the scope of an owner Specimen or SCG.
5
+ # This method relaxes {CaRuby::Resource#match_in_owner_scope} for a SCGEventParameters that matches any SpecimenEventParameters
6
+ # in others of the same class, since there can be at most one SCGEventParameters of a given class for a given SCG.
7
+ def match_in_owner_scope(others)
8
+ others.detect { |other| minimal_match?(other) }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module CaTissue
2
+ # import the Java class
3
+ java_import('edu.wustl.catissuecore.domain.Site')
4
+
5
+ # The Site domain class.
6
+ class Site
7
+ include Resource
8
+
9
+ # caTissue alert - the Site SCG collection is ignored, since it is not fetched with the Site,
10
+ # the caCORE query builder doesn't support abstract types, and even if it worked it would
11
+ # have limited value but high fetch cost. The work-around, which is also the more natural
12
+ # mechanism, is to query on a concrete SCG subclass template which references the target Site.
13
+ remove_attribute(:abstract_specimen_collection_groups)
14
+
15
+ set_secondary_key_attributes(:name)
16
+
17
+ add_attribute_defaults(:activity_status => 'Active', :site_type => 'Not Specified')
18
+
19
+ add_mandatory_attributes(:activity_status, :address, :coordinator, :site_type)
20
+
21
+ add_dependent_attribute(:address)
22
+
23
+ # The site_type value constants.
24
+ class SiteType
25
+ COLLECTION = 'Collection Site'
26
+ LABORATORY = 'Laboratory'
27
+ REPOSITORY = 'Repository'
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,456 @@
1
+ require 'caruby/util/inflector'
2
+ require 'caruby/util/uniquifier'
3
+ require 'caruby/util/validation'
4
+ require 'catissue/util/storable'
5
+
6
+ module CaTissue
7
+ # import the Java class
8
+ java_import('edu.wustl.catissuecore.domain.Specimen')
9
+
10
+ # The Specimen domain class.
11
+ class Specimen
12
+ include Validation, Storable, Resource
13
+
14
+ # caTissue alert - Bug #64: Some domain collection properties not initialized.
15
+ # Initialize consent_tier_statuses if necessary.
16
+ #
17
+ # @return [Java::JavaUtil::Set] the statuses
18
+ def consent_tier_statuses
19
+ getConsentTierStatusCollection or (self.consent_tier_statuses = Java::JavaUtil::LinkedHashSet.new)
20
+ end
21
+
22
+ # Sets the barcode to the given value. This method converts an Integer to a String.
23
+ def barcode=(value)
24
+ value = value.to_s if Integer === value
25
+ setBarcode(value)
26
+ end
27
+
28
+ add_attribute_aliases(:requirement => :specimen_requirement, :position => :specimen_position)
29
+
30
+ add_attribute_defaults(:activity_status => 'Active', :collection_status => 'Collected')
31
+
32
+ add_mandatory_attributes(:initial_quantity, :available_quantity)
33
+
34
+ set_secondary_key_attributes(:label)
35
+
36
+ # The Specimen-SpecimenRequirement association is bi-directional.
37
+ set_attribute_inverse(:specimen_requirement, :specimens)
38
+
39
+ # Specimen children are constrained to Specimen.
40
+ set_attribute_type(:child_specimens, Specimen)
41
+
42
+ # Specimen parent is constrained to Specimen.
43
+ set_attribute_type(:parent_specimen, Specimen)
44
+
45
+ # Even though the inverse is declared in AbstractSpecimen, do so again here to
46
+ # ensure that there is no order dependency of the dependent declaration below
47
+ # on AbstractSpecimen metadata initialization.
48
+ set_attribute_inverse(:parent_specimen, :child_specimens)
49
+
50
+ # A child Specimen is auto-generated from a SpecimenRequirement template if it
51
+ # is part of a hierarchy built by SCG create. Unlike SpecimenRequirement,
52
+ # Specimen children are cascaded.
53
+ add_dependent_attribute(:child_specimens, :autogenerated, :no_cascade_update_to_create)
54
+
55
+ # caTissue alert - Specimen consent_tier_statuses is cascaded but not fetched.
56
+ add_dependent_attribute(:consent_tier_statuses, :unfetched)
57
+
58
+ # caTissue alert - Bug #163: ExternalIdentifer not fetched with Specimen in API.
59
+ # Although the caTissue 1.1.2 Hibernate config specifies that external_identifiers
60
+ # are cascaded and fetched with the owner Specimen, they are not fetched with
61
+ # the owner Specimen. Work-around is to marked them as a unfetched.
62
+ #
63
+ # caTissue has complicated undocumented logic for updating external identifiers
64
+ # in NewSpecimenBizLogic.setExternalIdentifier
65
+ add_dependent_attribute(:external_identifiers, :unfetched)
66
+
67
+ # caTissue alert - Specimen position update is cascaded in Hibernate, but updateObject
68
+ # is precluded by the caTissue business logic. Position change is performed by a
69
+ # TransferEventParameters proxy instead. SpecimenPosition work-around is to
70
+ # designate a save proxy.
71
+ add_dependent_attribute(:specimen_position)
72
+
73
+ # Although label is the key, it is auto-generated if not provided in the create.
74
+ qualify_attribute(:label, :optional)
75
+
76
+ # The available flag is set on the server.
77
+ qualify_attribute(:is_available, :volatile)
78
+
79
+ # Oddly, the seldom-used biohazards are fetched along with Specimen.
80
+ qualify_attribute(:biohazards, :fetched)
81
+
82
+ # caTissue alert - the Specimen parent_changed flag is ignored in a caCORE update
83
+ # or create Specimen argument and is set on the server. Mark the attribute as unsaved.
84
+ qualify_attribute(:parent_changed, :unsaved)
85
+
86
+ # caTissue alert - Bug #159: Update pending Specimen ignores availableQuantity.
87
+ # available_quantity is not reflected in the caCORE create or update result.
88
+ # This is true even though the caTissue GUI supports available_quantity update.
89
+ # Work-around is to set the :autogenerated flag, which will refetch a saved Specimen
90
+ # and reupdate the Specimen if the stored available_quantity differs from the save
91
+ # argument.
92
+ qualify_attribute(:available_quantity, :autogenerated)
93
+
94
+ MERGEABLE_RQMT_ATTRS = nondomain_java_attributes - primary_key_attributes
95
+
96
+ MERGEABLE_SPC_CHR_ATTRS = SpecimenCharacteristics.nondomain_java_attributes - SpecimenCharacteristics.primary_key_attributes
97
+
98
+ # Specimen storage is constrained on the basis of the +specimen_class+.
99
+ alias :storable_type :specimen_class
100
+
101
+ def initialize(params=nil)
102
+ super
103
+ # work around caTissue Bug #64
104
+ self.consent_tier_statuses ||= Java::JavaUtil::LinkedHashSet.new
105
+ end
106
+
107
+ # Overrides {Resource#owner} to return the parent_specimen, if it exists, or the specimen_collection_group otherwise.
108
+ def owner
109
+ parent_specimen or specimen_collection_group
110
+ end
111
+
112
+ # @return [Boolean] whether this Specimen collection status is +Pending+
113
+ def pending?
114
+ collection_status == 'Pending'
115
+ end
116
+
117
+ # @return [Boolean] whether this Specimen collection status is +Collected+
118
+ def collected?
119
+ collection_status == 'Collected'
120
+ end
121
+
122
+ def merge_attribute_value(attribute, oldval, newval)
123
+ # caTissue alert - remove the autogenerated blank ExternalIdentifier.
124
+ # @see https://cabig-kc.nci.nih.gov/Biospecimen/forums/viewtopic.php?f=19&t=436&sid=ef98f502fc0ab242781b7759a0eaff36
125
+ # @see CaTissue::Database#query_safe
126
+ if attribute == :external_identifiers and newval then
127
+ CaTissue::Specimen.remove_empty_external_identifier(newval)
128
+ end
129
+ super
130
+ end
131
+
132
+ # # Restores this disposed Specimen by deleting the DisposalEventParameters and resetting the availability and activity status.
133
+ # # Returns the deleted DisposalEventParameters, or nil if none.
134
+ # def recover
135
+ # # TODO - test this
136
+ # dep = event_parameters.detect { |ep| CaTissue::DisposalEventParameters === ep }
137
+ # return if dep.nil?
138
+ # dep.delete
139
+ # self.available = true
140
+ # self.activity_status = 'Active'
141
+ # update
142
+ # end
143
+
144
+ # Override default {CaRuby::Resource#merge_attributes} to ignore a source SpecimenRequirement parent_specimen.
145
+ def merge_attributes(other, attributes=nil)
146
+ case other
147
+ when SpecimenRequirement then
148
+ # merge with the default requirement merge attributes if necessary
149
+ attributes ||= MERGEABLE_RQMT_ATTRS
150
+ super(other, attributes)
151
+ # copy the requirement characteristics
152
+ sc = other.specimen_characteristics
153
+ self.specimen_characteristics ||= sc.copy(MERGEABLE_SPC_CHR_ATTRS) if sc
154
+ when Hash then
155
+ # the requirement template
156
+ rqmt = other[:specimen_requirement] || other[:requirement]
157
+ # merge the attribute => value hash
158
+ super
159
+ # merge the SpecimenRequirement after the hash
160
+ merge_attributes(rqmt) if rqmt
161
+ else
162
+ super
163
+ end
164
+ self
165
+ end
166
+
167
+ # Raises a ValidationError when one the following conditions holds:
168
+ # * a top-level Specimen does not have a SGC
169
+ # * the available_quantity exceeds the initial_quantity
170
+ # * the availability flag is set and the available_quantity is zero
171
+ #
172
+ # caTissue alert - Bug #160: Missing Is Available? validation.
173
+ # Updating Specimen with the availablity flag set and available_quantity zero
174
+ # silently leaves the availablity flag unset.
175
+ def validate
176
+ super
177
+ if parent.nil? and specimen_collection_group.nil? then
178
+ raise ValidationError.new("Top-level specimen #{self} is missing specimen collection group")
179
+ end
180
+ if available_quantity and initial_quantity and available_quantity > initial_quantity then
181
+ raise ValidationError.new("#{self} available quantity #{available_quantity} cannot exceed initial quantity #{initial_quantity}")
182
+ end
183
+ if available? and available_quantity.zero? then
184
+ raise ValidationError.new("#{self} availablility flag cannot be set when the avaialble quantity is zero.")
185
+ end
186
+ if collected? then
187
+ unless event_parameters.detect { |ep| CaTissue::CollectionEventParameters === ep } then
188
+ raise ValidationError.new("#{self} is missing CollectionEventParameters.")
189
+ end
190
+ unless event_parameters.detect { |ep| CaTissue::ReceivedEventParameters === ep } then
191
+ raise ValidationError.new("#{self} is missing ReceivedEventParameters.")
192
+ end
193
+ end
194
+ end
195
+
196
+ # Returns the Specimen in others which matches this Specimen in the scope of an owner SCG.
197
+ # This method relaxes {CaRuby::Resource#match_in_owner_scope} to include a match on at least one external identifier.
198
+ def match_in_owner_scope(others)
199
+ super or others.detect do |other|
200
+ external_identifiers.any? do |eid|
201
+ other.external_identifiers.detect { |oeid| eid.name == oeid.name and eid.value == oeid.value }
202
+ end
203
+ end
204
+ end
205
+
206
+ # @return the SpecimenPosition class which this Specimen's Storable can occupy
207
+ def position_class
208
+ CaTissue::SpecimenPosition
209
+ end
210
+
211
+ # @return this Specimen +position+ Location
212
+ def location
213
+ position.location if position
214
+ end
215
+
216
+ # Creates a new Specimen or CaTissue::SpecimenRequirement from the given symbol => value params hash.
217
+ #
218
+ # The default class is inferred from the _class_ parameter, if given, or inherited
219
+ # from this parent specimen otherwise. The inferred class is the camel-case parameter value
220
+ # with +Specimen+ appended, e.g. :tissue => +TissueSpecimen+. This class name is resolved to
221
+ # a class in the CaTissue module context.
222
+ #
223
+ # The supported :type parameter value includes the permissible caTissue specimen type String
224
+ # values as well as the shortcut tissue type symbols :fresh, :fixed and :frozen.
225
+ #
226
+ # If a SpecimenRequirement parameter is provided, then that SpecimenRequirement's attribute
227
+ # values are merged into the new Specimen after the other parameters are merged. Thus, params
228
+ # takes precedence over the SpecimenRequirement.
229
+ #
230
+ # If the :count parameter is set to a number greater than one, then the specimen is aliquoted
231
+ # into the specified number of samples.
232
+ #
233
+ # This method is a convenience method to create either a Specimen or CaTissue::SpecimenRequirement.
234
+ # Although CaTissue::SpecimenRequirement is a direct CaTissue::AbstractSpecimen subclass rather than
235
+ # a Specimen subclass, the create functionality overlaps and Specimen is the friendlier
236
+ # class to define this utility method as opposed to the more obscure CaTissue::AbstractSpecimen.
237
+ #
238
+ # @param [<{Symbol => Object}>] params the create parameter hash. Besides the listed params options,
239
+ # this hash can include additional target Specimen attribute => value entries for any supported
240
+ # Specimen Java property attribute
241
+ # @option params [Symbol, String, Class] :class the required target specimen class,
242
+ # e.g. :molecular, +TissueSpecimen+ or CaTissue::TissueSpecimen
243
+ # @option params [Symbol, String, Class] :type the optional target specimen type symbol or string,
244
+ # e.g. :frozen or +Frozen Tissue Block+
245
+ # @option params [Numeric] :quantity the optional target specimen intial quantity
246
+ # @option params [CaTissue::SpecimenRequirement] :requirement the optional requirement with additional
247
+ # target attribute values
248
+ # @raise [ArgumentError] if the specimen class option is not recognized
249
+ def self.create_specimen(params)
250
+ raise ArgumentError.new("Specimen create params argument type unsupported: #{params.class}") unless Hash === params
251
+ # standardize the class, type and quantity params
252
+ spc_cls = params.delete(:class)
253
+ params[:specimen_class] ||= spc_cls if spc_cls
254
+ spc_type = params.delete(:type)
255
+ params[:specimen_type] ||= spc_type if spc_type
256
+ qty = params.delete(:quantity)
257
+ params[:initial_quantity] ||= qty if qty
258
+
259
+ # the specimen_class as a Class, Symbol or String
260
+ cls_opt = params[:specimen_class]
261
+ # standardize the specimen_class parameter as a permissible caTissue value
262
+ standardize_class_parameter(params)
263
+ # if the specimen_class was not specified as a Class, then infer the specimen domain class from the
264
+ # parameter prefix and Specimen suffix
265
+ if Class === cls_opt then
266
+ klass = cls_opt
267
+ else
268
+ class_name = params[:specimen_class] + 'Specimen'
269
+ klass = CaTissue.domain_type_with_name(class_name)
270
+ raise ArgumentError.new("Specimen class #{class_name} is not recognized for parameter #{cls_opt}") if klass.nil?
271
+ end
272
+
273
+ # add a default available quantity to a Specimen but not a SpecimenRequirement
274
+ params[:available_quantity] ||= params[:initial_quantity] if klass <= self
275
+
276
+ # make the specimen
277
+ klass.new(params)
278
+ end
279
+
280
+ # Convenience method which returns the SCG collection protocol.
281
+ #
282
+ # @return [CaTissue::CollectionProtocol] the SCG collection protocol
283
+ def collection_protocol
284
+ specimen_collection_group.collection_protocol
285
+ end
286
+
287
+ # Withdraws consent for this Specimen.
288
+ #
289
+ # _Experimental_. TODO - test this method.
290
+ #
291
+ # If a consent_tier is provided, then the SCG CaTissue::ConsentTierStatus with this consent tier is withdrawn.
292
+ # Otherwise, if there is a single SCG CaTissue::ConsentTierStatus, then that consent tier is withdrawn.
293
+ # Otherwise an exception is thrown.
294
+ #
295
+ # @param [CaTissue::ConsentTier, nil] optional consent tier of the SCG CaTissue::ConsentTierStatus to withdraw
296
+ # @raise [ValidationError] if an unambiguous SCG CaTissue::ConsentTierStatus to withdraw could not be determined
297
+ def withdraw_consent(consent_tier=nil)
298
+ statuses = specimen_collection_group.consent_tier_statuses
299
+ status = if consent_tier then
300
+ statuses.detect { |cts| cts.consent_tier.identifier == consent_tier.identifier } or
301
+ raise ValidationError.new("SCG #{specimen_collection_group} consent status not found for consent '#{consent_tier.statement}'")
302
+ elsif specimen_collection_group.consent_tier_statuses.size == 1 then
303
+ statuses.first
304
+ elsif specimen_collection_group.consent_tier_statuses.size == 0 then
305
+ raise ValidationError.new("Specimen #{self} SCG does not have a consent tier status.")
306
+ else
307
+ raise ValidationError.new("Specimen #{self} SCG consent tier is ambiguous:#{consent_tier_statuses.select { |cts| "\n #{cts.statement}" }.to_series('or')}.")
308
+ end
309
+ ct = status.consent_tier
310
+ cts = consent_tier_statuses.detect { |item| item.consent_tier == ct }
311
+ consent_tier_statuses << cts = ConsentTierStatus.new(:consent_tier => ct) if cts.nil?
312
+ cts.status = 'Withdrawn'
313
+ end
314
+
315
+ # Permanently dispose of this specimen by creating a CaTissue::DisposalEventParameters with
316
+ # status 'Closed' and the optional reason.
317
+ def dispose(reason=nil)
318
+ CaTissue::DisposalEventParameters.new(:specimen => self, :reason => reason)
319
+ end
320
+
321
+ protected
322
+
323
+ def self.remove_empty_external_identifier(eids)
324
+ bogus = eids.detect { |eid| eid.name.nil? }
325
+ if bogus then
326
+ logger.debug { "Work around caTissue bug by removing empty fetched #{bogus.specimen.qp} #{bogus.qp} from #{eids.qp}..." }
327
+ # dissociate the specimen
328
+ bogus.specimen = nil
329
+ # remove the bogus eid
330
+ eids.delete(bogus)
331
+ end
332
+ eids
333
+ end
334
+
335
+ private
336
+
337
+ # Adds this Specimen's defaults, as follows:
338
+ # * The default specimen_collection_group is the parent specimen_collection_group.
339
+ # * Add default collection and received event parameters if this Specimen is collected.
340
+ # * If the is_available flag is set to false then the default available quantity is
341
+ # zero, otherwise the default available quantity is the initial quantity.
342
+ # * The default is_available flag is true if the available quantity is greater than zero.
343
+ #
344
+ # The motivation for the is_available flag default is that an initial quantity of zero can indicate
345
+ # unknown amount as well a known zero amount, and therefore the available flag should be set to
346
+ # false only if it is known that there was an amount but that amount is exhausted.
347
+ #
348
+ # caTissue alert - initial_quantity cannot be null (cf. Bug #160).
349
+ #
350
+ # caTissue alert - the default available status must be left nil rather than set to false, since
351
+ # caTissue allows a nil available status on insert but not a false value, even though a nil status
352
+ # is set to false (0 database value) when the record is inserted.
353
+ #
354
+ # caTissue alert - a collected Specimen without a collection and received event parameters
355
+ # results in the dreaded 'Severe Error' server message. TODO - isolate and report.
356
+ def add_defaults_local
357
+ super
358
+ self.specimen_collection_group ||= parent.specimen_collection_group if parent
359
+ add_default_event_parameters
360
+
361
+ # The default available quantity is 0 if the is_available flag is false,
362
+ # otherwise the initial quantity.
363
+ self.available_quantity ||= is_available == false ? 0.0 : initial_quantity
364
+
365
+ # The specimen is available by default if there is a positive available quantity.
366
+ # If is_available is set to false, then set it to nil to work around a caTissue bug.
367
+ self.is_available ||= available_quantity.zero? ? nil : true
368
+ end
369
+
370
+ # Adds the default collection and received event parameters if the collection status
371
+ # is +Collected+.
372
+ def add_default_event_parameters
373
+ if collection_status == 'Collected' and specimen_collection_group then
374
+ unless event_parameters.detect { |ep| CaTissue::CollectionEventParameters === ep } then
375
+ CaTissue::CollectionEventParameters.new(:specimen => self, :user => specimen_collection_group.collector)
376
+ end
377
+ unless event_parameters.detect { |ep| CaTissue::ReceivedEventParameters === ep } then
378
+ CaTissue::ReceivedEventParameters.new(:specimen => self, :user => specimen_collection_group.receiver)
379
+ end
380
+ end
381
+ end
382
+
383
+ # Sets the :specimen_class parameter to a permissible caTissue value.
384
+ def self.standardize_class_parameter(params)
385
+ opt = params[:specimen_class]
386
+ if opt.nil? then
387
+ rqmt = params[:specimen_requirement] || params[:requirement]
388
+ if rqmt then
389
+ rqmt.add_defaults unless rqmt.specimen_class
390
+ opt = rqmt.specimen_class
391
+ end
392
+ end
393
+ raise ArgumentError.new("Specimen class is missing from the create parameters") if opt.nil?
394
+ # Convert the class option Symbol to a capitalized String without a class path prefix or the
395
+ # Specimen[Requirement] suffix.
396
+ params[:specimen_class] = opt.to_s[/(\w+?)(Specimen(Requirement)?)?$/, 1].capitalize_first
397
+ end
398
+
399
+ def set_aliquot_parameters(params, count)
400
+ super
401
+ # default available quantity
402
+ self.available_quantity ||= initial_quantity
403
+ # apportion the parent quantity
404
+ params[:initial_quantity] ||= available_quantity / count
405
+ end
406
+
407
+ # Delegate to {AbstractSpecimen#create_derived} and add a default label if necessary. The default label
408
+ # is this Specimen label appended with an underscore and the number of children, e.g. +TB-0023434_1+
409
+ # for the first child of a parent with label +TB-0023434+.
410
+ def create_derived(params)
411
+ spc = super
412
+ spc.label ||= "#{label}_#{children.size}" if label
413
+ spc.specimen_collection_group = specimen_collection_group
414
+ # if the derived specimen is the same type as this parent specimen,
415
+ # then decrement this parent's quantity by the derived specimen amount
416
+ decrement_derived_quantity(spc) if specimen_type == spc.specimen_type
417
+ spc
418
+ end
419
+
420
+ def self.specimen_class_symbol_to_class(symbol)
421
+ name = symbol.to_s
422
+ class_name = name[0, 1].upcase + name[1..-1]
423
+ suffix = 'Specimen'
424
+ class_name << suffix unless class_name.rindex(suffix) == class_name.length - suffix.length
425
+ CaTissue.domain_type_with_name(class_name) or raise ArgumentError.new("Specimen class #{class_name} is not recognized for specimen type parameter #{symbol}")
426
+ end
427
+
428
+ # def default_specimen_characteristics
429
+ # return if specimen_collection_group.nil?
430
+ # rqmts = specimen_collection_group.requirements
431
+ # # match the requirement specimen type
432
+ # rqmt = specimen_collection_group.requirements.detect { |rqmt| specimen_type == rqmt.specimen_type }
433
+ # # fallback is match on requirement class and generic requirment specimen type
434
+ # rqmt ||= specimen_collection_group.requirements.detect do |rqmt|
435
+ # specimen_class === rqmt.specimen_class and rqmt.specimen_type.nil? or rqmt.specimen_type == 'Not Specified'
436
+ # end
437
+ # rqmt.specimen_characteristics.copy if rqmt
438
+ # end
439
+
440
+ # Decrements this parent's available quantity by the given child's initial quantity, if the specimen types are the same and there
441
+ # are the relevant quantities.
442
+ def decrement_derived_quantity(child)
443
+ return unless specimen_type == child.specimen_type and child.initial_quantity
444
+ if available_quantity.nil? then
445
+ raise ValidationError.new("Derived specimen has an initial quantity #{child.initial_quantity} but the parent is missing an available quantity")
446
+ elsif (available_quantity - child.initial_quantity).abs < 0.00000001 then
447
+ # rounding error
448
+ self.available_quantity = 0.0
449
+ elsif child.initial_quantity <= available_quantity then
450
+ self.available_quantity -= child.initial_quantity
451
+ else
452
+ raise ValidationError.new("Derived specimen initial quantity #{child.initial_quantity} exceeds parent available quantity #{available_quantity}")
453
+ end
454
+ end
455
+ end
456
+ end