ion_orders_engine_mockingjay 1.0.1.SNAPSHOT → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/models/concerns/mock/import_concern.rb +332 -0
- data/app/models/concerns/mock/phone_number_concern.rb +33 -0
- data/app/models/concerns/mock/resource_ancestors_concern.rb +86 -0
- data/app/models/concerns/mock/root_model_concern.rb +41 -0
- data/config/application.rb +2 -4
- data/config/initializers/reference.rb +0 -23
- data/config/routes.rb +2 -2
- data/db/development.sqlite3 +0 -0
- data/lib/bootstrap_breadcrumbs_builder.rb +34 -0
- data/lib/enums/duplicate_strategy.rb +25 -0
- data/lib/enums.rb +4 -0
- data/lib/interchange/Writer.rb +176 -0
- data/lib/interchange/reader.rb +197 -0
- data/lib/mixins/logging.rb +36 -0
- data/lib/reference/data_types.rb +31 -0
- data/lib/reference/loader.rb +46 -0
- data/lib/reference/updater.rb +23 -0
- data/lib/resource_display_helper.rb +46 -0
- data/lib/tasks/auto_annotate_models.rake +20 -0
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 09186104869a21657277807bfcccb3f0d85b4ac1
|
4
|
+
data.tar.gz: b11ee2ade83e151b3159d832806f0ab0e0cd47de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc2e73a77bf98a169ff2a3e74f8ebc324e44b8f2cd1b692698f430d0a20e5cedc80310945a427b0a0bf488fc618b01b806758420ec94eb2dc3de4e0d16fea261
|
7
|
+
data.tar.gz: 202a3931a931c60cf12b27c54c038e04270e6ba36fa950b2a46b20a8a55ee7710933ec5de26deff931f3094a9b73f33d3acf3f874d8b232b60a7ee5dfe8eb542
|
@@ -0,0 +1,332 @@
|
|
1
|
+
# Public: We provide behavior that allows for the creation of new instances
|
2
|
+
# of a model from either a JSON String or a Ruby Hash.
|
3
|
+
#
|
4
|
+
# Include this module on every model built in this app.
|
5
|
+
module Mock::ImportConcern
|
6
|
+
# Extensions
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
end
|
11
|
+
|
12
|
+
# ---------- Instance Methods ----------
|
13
|
+
|
14
|
+
# No instance methods present in this mixin.
|
15
|
+
|
16
|
+
|
17
|
+
# Public: Checks to see if we have a has_one relation of the specified name.
|
18
|
+
# The perspective is that we are the owning resource and we are advising
|
19
|
+
# if we have a has_one association with the given name.
|
20
|
+
#
|
21
|
+
# relation_name - A String or Symbol, such as 'policy' or :policy
|
22
|
+
#
|
23
|
+
# Returns Boolean true if the model we're mixed into has a 'has_one' relation with the given name.
|
24
|
+
def has_a_has_one_relation?(relation_name)
|
25
|
+
has_one_keys = self.class.has_one_relations_hash.keys
|
26
|
+
sanitized_relation_name = relation_name.to_s
|
27
|
+
has_one_keys.include? sanitized_relation_name
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Public: Determines if we are the destination of a has_one relation from our parent
|
32
|
+
# resource, assuming we even have a parent resource.
|
33
|
+
def is_singular_child_resource?
|
34
|
+
parent_model = self.parent_resource_model_object
|
35
|
+
if parent_model
|
36
|
+
parent_model.has_a_has_one_relation? self.class.to_s.tableize.singularize
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
# ---------- Class Methods ----------
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
|
46
|
+
# Public: Provides an array of those attributes whose values together, determine non-synthetic uniqueness.
|
47
|
+
# We define an empty array here. Classes that use this module mixin should override with there own list of
|
48
|
+
# keys as symbols. Never include the :id attribute, because that is synthetic.
|
49
|
+
#
|
50
|
+
# Returns Array of symbols representing attributes that together, are a business compound primary key.
|
51
|
+
def uniqueness_attributes
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Public: Takes a hash of attributes and pulls out just those that match our uniqueness attributes.
|
57
|
+
# The resultant hash can be used as conditions in a <emphasis>where</emphasis> clause to query with.
|
58
|
+
#
|
59
|
+
# contents_hash - The hash of attributes for a candidate instance of this model.
|
60
|
+
#
|
61
|
+
# Returns Hash containing properties as key-value pairs, which uniquely identify an instance of the mixed
|
62
|
+
# in model.
|
63
|
+
def uniqueness_conditions(contents_hash)
|
64
|
+
conditions_hash = {}
|
65
|
+
log_info "Mock::ImportConcern: uniqueness_conditions(): uniqueness_attributes to work with is: #{uniqueness_attributes}"
|
66
|
+
uniqueness_attributes.each do | unique_attribute_key |
|
67
|
+
#log "Mock::ImportConcern: uniqueness_conditions(): Searching for key: #{unique_attribute_key}"
|
68
|
+
# The contents hash we'll be looking through is JSON, with string keys instead of symbol keys,
|
69
|
+
# and from testing, apparently that matters! Hence, we do a 'to_s' on each key before looking
|
70
|
+
# through the contents_hash for it:
|
71
|
+
search_value = contents_hash[unique_attribute_key.to_s]
|
72
|
+
if search_value
|
73
|
+
#log "Mock::ImportConcern: uniqueness_conditions(): Found value #{search_value} for key: #{unique_attribute_key}"
|
74
|
+
conditions_hash[unique_attribute_key] = search_value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
conditions_hash
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
# Public: Creates a hash that maps relationship name to model class constant, for our has_one relations.
|
83
|
+
#
|
84
|
+
# Returns Hash of the mixing in model's ActiveRecord has_one relations with relation names as keys
|
85
|
+
# and destination classes as values.
|
86
|
+
def has_one_relations_hash
|
87
|
+
relations_reflection = self.reflect_on_all_associations(:has_one)
|
88
|
+
relations_hash = {}
|
89
|
+
relations_reflection.each do |relation|
|
90
|
+
relations_hash[relation.name.to_s] = relation.class_name.constantize
|
91
|
+
end
|
92
|
+
|
93
|
+
#log "has_one_relations_hash: #{relations_hash}"
|
94
|
+
relations_hash
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
# Public: Creates a hash that maps relationship name to model class constant, for our has_many relations.
|
99
|
+
#
|
100
|
+
# Returns Hash of the mixing in model's ActiveRecord has_many relations with relation names as keys
|
101
|
+
# and destination classes as values.
|
102
|
+
def has_many_relations_hash
|
103
|
+
relations_reflection = self.reflect_on_all_associations(:has_many)
|
104
|
+
relations_hash = {}
|
105
|
+
relations_reflection.each do |relation|
|
106
|
+
relations_hash[relation.name.to_s] = relation.class_name.constantize
|
107
|
+
end
|
108
|
+
|
109
|
+
#log "has_many_relations_hash: #{relations_hash}"
|
110
|
+
relations_hash
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Public: Takes a Hash of attributes for this model, and does a cross check if there exists
|
115
|
+
# an instance of this model already with the same values as those passed in. In doing our check,
|
116
|
+
# we <strong>only</strong> consider those attributes present in the array returned from a call
|
117
|
+
# to uniqueness_attributes().
|
118
|
+
#
|
119
|
+
# contents_hash - The hash of attributes for a candidate instance of this model.
|
120
|
+
#
|
121
|
+
# Returns Boolean whether or not a model instance exists with the provided contents hash.
|
122
|
+
def has_existing_instance?(contents_hash)
|
123
|
+
conditions_hash = uniqueness_conditions(contents_hash)
|
124
|
+
#log "#{self.to_s}: Looking for existing instance using contents_hash: #{contents_hash}"
|
125
|
+
#log "#{self.to_s}: Looking for existing instance with attributes: #{conditions_hash}"
|
126
|
+
|
127
|
+
# Do we have an empty set of conditions?
|
128
|
+
if conditions_hash.empty?
|
129
|
+
# YES: Return a no match result. This shouldn't happen, but if we allow this answer,
|
130
|
+
# every record will be matched with the no-constraint conditions, and then possibly removed.
|
131
|
+
log_warn "Interchange::ImportConcern has_existing_instance(): Ran into situation where conditions_hash" \
|
132
|
+
"computed was empty. This is dangerous; a wildcard match like this could cause us to delete all" \
|
133
|
+
"existing records as duplicates. Ensure uniqueness_attributes array on root models are set correctly."
|
134
|
+
false
|
135
|
+
else
|
136
|
+
# NO: We have some conditions that we can filter on, so we'll do a search based on these
|
137
|
+
# conditions and let the existence thereof be our verdict.
|
138
|
+
self.where(conditions_hash).exists?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# Internal: Retrieves all instances of this model that match a subset of the values in the
|
144
|
+
# contents hash; specifically, the uniqueness values.
|
145
|
+
#
|
146
|
+
# contents_hash - The hash of attributes for a candidate instance of this model.
|
147
|
+
#
|
148
|
+
# Return Array of all instances of this model that matched a subset of the values in the contents hash, as
|
149
|
+
# defined by the uniqueness_attributes
|
150
|
+
def retrieve_existing_instances(contents_hash)
|
151
|
+
conditions_hash = uniqueness_conditions(contents_hash)
|
152
|
+
self.where(conditions_hash)
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
# Internal: Returns the first matching existing instance of this model found, otherwise nil.
|
157
|
+
#
|
158
|
+
# contents_hash - The hash of attributes for a candidate instance of this model.
|
159
|
+
#
|
160
|
+
# Returns ActiveRecord::Base an instance of this model that matched a subset of the values in the contents hash
|
161
|
+
def retrieve_any_existing_instance(contents_hash)
|
162
|
+
existing = retrieve_existing_instances(contents_hash)
|
163
|
+
unless existing.empty?
|
164
|
+
existing.first
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
# Internal: Finds existing instances of this model that match the uniqueness attributes defined for this model,
|
170
|
+
# and destroys them. Presumably, this is to make way for a newly imported instance that would then be a dupe.
|
171
|
+
#
|
172
|
+
# contents_hash - the hash of attributes for a candidate instance of this model.
|
173
|
+
#
|
174
|
+
# Returns Fixnum the number of instances we found (and presumably destroyed).
|
175
|
+
def remove_duplicates(contents_hash)
|
176
|
+
existing_instances = retrieve_existing_instances(contents_hash)
|
177
|
+
existing_instances.each do | existing_instance |
|
178
|
+
log "#{self.to_s}: remove_duplicates(): Destroying instance #{existing_instance.id}"
|
179
|
+
existing_instance.destroy
|
180
|
+
end
|
181
|
+
|
182
|
+
existing_instances.count
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
# Public: Performs an import operation on the mixing in model, using the model contents provided.
|
187
|
+
#
|
188
|
+
# Note: when we ourselves call import() on related models, we effectively ignore the duplicate_strategy, as
|
189
|
+
# those related models will by definition, always be non-root models. We only care about duplicates when it comes
|
190
|
+
# to models that define top level collections (i.e. root models).
|
191
|
+
#
|
192
|
+
# contents - the contents of a new model to be imported. This may be a proper Ruby Hash.
|
193
|
+
# duplicate_strategy - a constant from the Enums::DuplicateStrategy module. This indicates whether duplicates
|
194
|
+
# should be allowed, skipped or replace that which they duplicate.
|
195
|
+
# This is only meaningful when importing a root model.
|
196
|
+
# root_model - if not specified, we'll eventually set it by walking up the parent resource chain
|
197
|
+
# on the model we do instantiate (default: parent resource).
|
198
|
+
#
|
199
|
+
# Returns ActiveRecord::Base the model instance we just built.
|
200
|
+
def import(contents, duplicate_strategy, root_model=nil)
|
201
|
+
#log "#{self.to_s}: Asked to import data."
|
202
|
+
|
203
|
+
# Convent the contents passed in to a Hash if it is currently a String of (presumably) JSON.
|
204
|
+
#if contents.is_a? String
|
205
|
+
# log 'We were passed a String of contents'
|
206
|
+
# contents_hash = JSON.parse(contents)
|
207
|
+
#else
|
208
|
+
# log 'We were passed a Hash of contents'
|
209
|
+
# contents_hash = contents
|
210
|
+
#end
|
211
|
+
|
212
|
+
contents_hash = contents
|
213
|
+
model = self.new
|
214
|
+
|
215
|
+
unless root_model
|
216
|
+
if model.is_root_resource?
|
217
|
+
root_model = model
|
218
|
+
root_model.import_errors = [] # Initialize the list of errors
|
219
|
+
root_model.import_notices = [] # Initialize the list of notices
|
220
|
+
|
221
|
+
# Determine if we have duplicates, and if so, handle them as directed by duplicate_strategy:
|
222
|
+
apply_duplicate_handling_strategy(contents_hash, duplicate_strategy, root_model)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Apply values from the contents hash onto the model:
|
227
|
+
apply_content_to_model(contents_hash, model, root_model)
|
228
|
+
|
229
|
+
# Return the new model just built; callers are responsible for saving, when appropriate.
|
230
|
+
model
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
# Public: Determines whether we should allow, replace or skip duplicates. We'll first search for a duplicate
|
235
|
+
# and if one exists, we'll then apply the duplicate strategy provided in determining how to deal with
|
236
|
+
# the duplicate. As we go, we'll add entries to the root model's import_notices array if warranted.
|
237
|
+
# For imports that should be skipped, we set the skip_import flag on the root model indicating that
|
238
|
+
# it should not be saved.
|
239
|
+
#
|
240
|
+
# contents_hash - the hash of attributes for a candidate instance of this model.
|
241
|
+
# duplicate_strategy - a constant from the Enums::DuplicateStrategy module. This indicates whether duplicates
|
242
|
+
# should be allowed, skipped or replace that which they duplicate. This is only meaningful
|
243
|
+
# when importing a root model.
|
244
|
+
# root_model - if not specified, we'll eventually set it by walking up the parent resource chain
|
245
|
+
# on the model we do instantiate.
|
246
|
+
#
|
247
|
+
# Returns nothing.
|
248
|
+
def apply_duplicate_handling_strategy(contents_hash, duplicate_strategy, root_model)
|
249
|
+
if has_existing_instance?(contents_hash)
|
250
|
+
case duplicate_strategy
|
251
|
+
when Enums::DuplicateStrategy::ALLOW
|
252
|
+
# Do nothing; allow it if there happened to be a duplicate.
|
253
|
+
# Log this in import_notices
|
254
|
+
root_model.import_notices << "Imported as an allowed duplicate."
|
255
|
+
when Enums::DuplicateStrategy::REPLACE
|
256
|
+
# Remove existing entries
|
257
|
+
num_dupes_removed = remove_duplicates(contents_hash)
|
258
|
+
# Log this in import_notices
|
259
|
+
root_model.import_notices << "Imported, but had to delete #{num_dupes_removed} duplicate(s) prior."
|
260
|
+
when Enums::DuplicateStrategy::SKIP
|
261
|
+
# Log this in import_notices
|
262
|
+
root_model.import_notices << "Skipped importing this, as it was a duplicate."
|
263
|
+
# Break out of this import; nothing more for us to do. We'll set a return flag to indicate this.
|
264
|
+
root_model.skip_import = true
|
265
|
+
else
|
266
|
+
# Do nothing. Assume our caller has already checked for valid duplicate_strategy values.
|
267
|
+
end # case
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
|
272
|
+
# Public: Applies the contents hash provided onto the model object provided. This includes
|
273
|
+
# core properties of the model, as well as related objects. For related objects,
|
274
|
+
# we effectively call into the import() method on the related class, building out
|
275
|
+
# our object graph with recursion.
|
276
|
+
#
|
277
|
+
# contents_hash - the hash of attributes for a candidate instance of this model.
|
278
|
+
# model - the model instance we are setting values into from the contents hash.
|
279
|
+
# root_model - represents the root model for the model we're currently importing. We need this
|
280
|
+
# for when we make re-entrant calls into the import() method for related
|
281
|
+
# models to our model instance.
|
282
|
+
#
|
283
|
+
# Returns nothing.
|
284
|
+
def apply_content_to_model(contents_hash, model, root_model)
|
285
|
+
has_many_relationships_hash = has_many_relations_hash()
|
286
|
+
has_one_relationships_hash = has_one_relations_hash()
|
287
|
+
|
288
|
+
# Any contents in hash not in a pair will generate an exception. Defend against such malformed input
|
289
|
+
# content with an exception handling block.
|
290
|
+
begin
|
291
|
+
contents_hash.each_pair do |key, value|
|
292
|
+
#log "Processing key from contents hash: '#{key}'"
|
293
|
+
|
294
|
+
next if key == 'id' # Defend against these, which really shouldn't be in the source input files anyways.
|
295
|
+
|
296
|
+
if model.has_attribute? key
|
297
|
+
#log "Processing attribute: #{key} | value: #{value}"
|
298
|
+
model.send("#{key}=", value) # Superior to instance_variable_set b/c we're dealing with ActiveRecord attributes
|
299
|
+
|
300
|
+
elsif has_one_relationships_hash.keys.include? key
|
301
|
+
#log "The key: #{key} is one of our has_one relations"
|
302
|
+
klazz = has_one_relationships_hash[key]
|
303
|
+
if klazz.respond_to? :import
|
304
|
+
#log "#{klazz.to_s} responds to import."
|
305
|
+
related_has_one_entity = klazz.import(value, Enums::DuplicateStrategy::ALLOW, root_model)
|
306
|
+
#log "++About to add '#{related_has_one_entity.class.name}' as a has one to '#{self.name}' via the key '#{key}'"
|
307
|
+
mutator_key = "#{key}=".to_sym
|
308
|
+
model.send(mutator_key, related_has_one_entity)
|
309
|
+
#log "..Completed adding '#{related_has_one_entity.class.name}' as a has one to '#{self.name}' via the key '#{key}'"
|
310
|
+
end
|
311
|
+
|
312
|
+
elsif has_many_relationships_hash.keys.include? key
|
313
|
+
#log "The key: #{key} is one of our has_many relations"
|
314
|
+
klazz = has_many_relationships_hash[key]
|
315
|
+
if klazz.respond_to? :import
|
316
|
+
#log "#{klazz.to_s} responds to import."
|
317
|
+
value.each do |related_object_data|
|
318
|
+
model.send(key) << klazz.import(related_object_data, Enums::DuplicateStrategy::ALLOW, root_model)
|
319
|
+
end
|
320
|
+
else
|
321
|
+
log "#{klazz.to_s} does NOT respond to import."
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
rescue Exception => exception
|
326
|
+
log_error "ERROR: Ran into an exception importing model #{self.to_s}. #{exception.class.to_s}: #{exception}."
|
327
|
+
root_model.import_errors << "#{exception.class.to_s}: #{exception.message}"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
end # module ClassMethods
|
332
|
+
end # module Mock::ImportConcern
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# This module provides a method that reformats our related phone numbers into a hash structure.
|
2
|
+
# This can be useful for many things, though our primary use is to facilitate generating
|
3
|
+
# JSON responses via JBuilder
|
4
|
+
#
|
5
|
+
# Include this module into any model that has_many :phone_numbers.
|
6
|
+
module Mock::PhoneNumberConcern
|
7
|
+
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
end
|
12
|
+
|
13
|
+
# Organizes all of our associated PhoneNumbers into a hash structure, where the key is the
|
14
|
+
# label and the value is an Array of all raw phone number values that have that given label.
|
15
|
+
# This facilitates situations where for example, you have two home phone numbers.
|
16
|
+
def formatted_phone_numbers_hash
|
17
|
+
numbers_hash = {}
|
18
|
+
phone_numbers.each do |phone_number_object|
|
19
|
+
iterated_label = phone_number_object.label
|
20
|
+
# Does the label already exist in our hash?
|
21
|
+
if numbers_hash[iterated_label]
|
22
|
+
# YES: Just add the number to the existing value array:
|
23
|
+
numbers_hash[iterated_label] << phone_number_object.number
|
24
|
+
else
|
25
|
+
# NO: Create a new entry for this label:
|
26
|
+
numbers_hash[iterated_label] = [phone_number_object.number]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
numbers_hash
|
31
|
+
end
|
32
|
+
|
33
|
+
end # module
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Include this module on every model we use. For non-root models, include the class
|
2
|
+
# declaration parent_resource with a symbol identifying what method an instance of that model
|
3
|
+
# would call to retrieve its resource parent.
|
4
|
+
#
|
5
|
+
# For example, in the Allergy.rb model file, we'd have the following line:
|
6
|
+
#
|
7
|
+
# parent_resource :patient
|
8
|
+
#
|
9
|
+
# This information allows us to build a model chain up to the root model; useful in constructing
|
10
|
+
# navigation structures, such as breadcrumbs.
|
11
|
+
#
|
12
|
+
module Mock::ResourceAncestorsConcern
|
13
|
+
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
def parent_resource_model_object
|
21
|
+
self.send(self.class.parent_resource_symbol) if self.class.parent_resource_symbol
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def resource_ancestors
|
26
|
+
parent = parent_resource_model_object
|
27
|
+
|
28
|
+
if parent
|
29
|
+
parent.resource_ancestors + [self]
|
30
|
+
else
|
31
|
+
[self]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
def root_resource
|
37
|
+
resource_ancestors.first unless resource_ancestors.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# We are a root resource if our root resource is ourselves.
|
42
|
+
def is_root_resource?
|
43
|
+
root_resource == self
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Returns nil by default. Models should override if they'd like
|
48
|
+
# a short summary to appear in breadcrumbs.
|
49
|
+
def short_title
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Returns a title string that is more than the short_title; it is preceded by the model class name.
|
55
|
+
def model_qualified_title
|
56
|
+
"#{self.class.to_s}: #{short_title}"
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# We are a protected record if the Patient we ultimately belong to is a protected record.
|
61
|
+
# Any root model must implement or have a property such that we can ask it 'protected_record?'.
|
62
|
+
# So as not to conflict, this mixin module uses the signature 'marked_as_protected?' to traverse
|
63
|
+
# up the parent resource chain.
|
64
|
+
def marked_as_protected?
|
65
|
+
parent = parent_resource_model_object
|
66
|
+
|
67
|
+
if parent
|
68
|
+
parent.marked_as_protected?
|
69
|
+
else
|
70
|
+
self.protected_record?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
def parent_resource(parent_resource_method_symbol)
|
77
|
+
@parent_resource_symbol = parent_resource_method_symbol.to_sym
|
78
|
+
end
|
79
|
+
|
80
|
+
def parent_resource_symbol
|
81
|
+
@parent_resource_symbol
|
82
|
+
end
|
83
|
+
end # module ClassMethods
|
84
|
+
|
85
|
+
|
86
|
+
end # module Mock::ResourceAncestorsConcern
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# This concern is only meant to be mixed into models that are top-level collections.
|
2
|
+
# That is, those models who have no parent model resource.
|
3
|
+
module Mock::RootModelConcern
|
4
|
+
|
5
|
+
# Extensions
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
end
|
10
|
+
|
11
|
+
# Instance Variables
|
12
|
+
attr_accessor :export_errors
|
13
|
+
attr_accessor :import_errors
|
14
|
+
attr_accessor :import_notices
|
15
|
+
attr_accessor :skip_import
|
16
|
+
|
17
|
+
|
18
|
+
# ---------- Instance Methods ----------
|
19
|
+
|
20
|
+
def skip_import?
|
21
|
+
skip_import
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Returns the name to be used for data interchange. Normally, one just returns
|
26
|
+
# the name of the class.
|
27
|
+
def interchange_type_name
|
28
|
+
self.class.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
# ---------- Class Methods ----------
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
# Our implementing classes should always respond in the affirmative to this query.
|
35
|
+
def is_root_resource?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end # Mock::RootModelConcern::ClassMethods
|
39
|
+
|
40
|
+
|
41
|
+
end # Mock::RootModelConcern
|
data/config/application.rb
CHANGED
@@ -38,7 +38,7 @@ module Mockingjay
|
|
38
38
|
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
|
39
39
|
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
|
40
40
|
config.active_record.default_timezone = :local
|
41
|
-
config.active_record.time_zone_aware_attributes = false
|
41
|
+
config.active_record.time_zone_aware_attributes = false
|
42
42
|
|
43
43
|
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
|
44
44
|
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
|
@@ -95,6 +95,4 @@ module Mockingjay
|
|
95
95
|
end
|
96
96
|
|
97
97
|
end
|
98
|
-
end
|
99
|
-
|
100
|
-
|
98
|
+
end
|
@@ -4,34 +4,11 @@ module Reference
|
|
4
4
|
# to Ruby classes concretely defined in app/models/reference. Only include those root level reference classes;
|
5
5
|
# any nested classes should be bootstrapped from the initialize() method of the parent reference class.
|
6
6
|
ROOT_REFERENCE_MODEL_CLASSES = [
|
7
|
-
Reference::LabGroup,
|
8
|
-
Reference::LabResultDataType,
|
9
|
-
Reference::AllergyReactionType,
|
10
|
-
Reference::ConditionStatusType,
|
11
|
-
Reference::NotificationConceptType,
|
12
|
-
Reference::AllergyStatusType,
|
13
|
-
Reference::AllergySeverity,
|
14
|
-
Reference::AllergyCancelReason,
|
15
|
-
Reference::VisitType,
|
16
|
-
Reference::VitalsMeasurementType,
|
17
|
-
Reference::VitalsMeasurementStatusType,
|
18
|
-
Reference::VitalsMeasurementNormalcy,
|
19
|
-
Reference::VitalsMeasurementUnit,
|
20
|
-
Reference::VitalsMeasurementContributorSystem,
|
21
|
-
Reference::VitalsMeasurementSystem,
|
22
|
-
Reference::VitalsMeasurementResultType,
|
23
|
-
Reference::DiagnosisConfirmationStatus,
|
24
|
-
Reference::ProblemClassification,
|
25
|
-
Reference::DiagType,
|
26
|
-
Reference::LifeCycleStatus,
|
27
|
-
Reference::ClinicalDiagnosisClinicalService,
|
28
|
-
Reference::HealthRecordCancelReason
|
29
7
|
]
|
30
8
|
|
31
9
|
# This Array holds only those classes that are nested reference models, (ultimately) dependent on one class in the
|
32
10
|
# ROOT_REFERENCE_MODEL_CLASSES Array.
|
33
11
|
NESTED_REFERENCE_MODEL_CLASSES = [
|
34
|
-
Reference::LabConcept
|
35
12
|
]
|
36
13
|
|
37
14
|
# Where reference data is read from and written to
|
data/config/routes.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Mockingjay
|
1
|
+
Mockingjay::Application.routes.draw do
|
2
2
|
match '/patients/:patientId/orders/inpatient' => 'api/ion_orders_engine/order_profile/inpatient_order_profile#show', via: :get, :constraints => {:patientId => /\d+/}
|
3
3
|
match '/patients/:patientId/orders/:orderId' => 'api/ion_orders_engine/order_by_id/order_by_id#show', via: :get, :constraints => {:patientId => /\d+/, :orderId => /\d+/}
|
4
|
-
end
|
4
|
+
end # Mockingjay::Application.routes.draw
|
File without changes
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# CREDIT: https://gist.github.com/1933884
|
2
|
+
#
|
3
|
+
# The BootstrapBreadcrumbsBuilder is a Bootstrap compatible breadcrumb builder.
|
4
|
+
# It provides basic functionalities to render a breadcrumb navigation according to Bootstrap's conventions.
|
5
|
+
#
|
6
|
+
# BootstrapBreadcrumbsBuilder accepts a limited set of options:
|
7
|
+
# * separator: what should be displayed as a separator between elements
|
8
|
+
#
|
9
|
+
# You can use it with the :builder option on render_breadcrumbs:
|
10
|
+
# <%= render_breadcrumbs :builder => ::BootstrapBreadcrumbsBuilder, :separator => "»" %>
|
11
|
+
#
|
12
|
+
# Note: You may need to adjust the autoload_paths in your config/application.rb file for rails to load this class:
|
13
|
+
# config.autoload_paths += Dir["#{config.root}/lib/"]
|
14
|
+
#
|
15
|
+
class BootstrapBreadcrumbsBuilder < BreadcrumbsOnRails::Breadcrumbs::Builder
|
16
|
+
def render
|
17
|
+
@context.content_tag(:ul, class: 'breadcrumb') do
|
18
|
+
@elements.collect do |element|
|
19
|
+
render_element(element)
|
20
|
+
end.join.html_safe
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def render_element(element)
|
25
|
+
current = @context.current_page?(compute_path(element))
|
26
|
+
|
27
|
+
@context.content_tag(:li, class: ('active' if current)) do
|
28
|
+
link_or_text = @context.link_to_unless_current(compute_name(element), compute_path(element), element.options)
|
29
|
+
divider = @context.content_tag(:span, (@options[:separator] || '/').html_safe, class: 'divider') unless current
|
30
|
+
|
31
|
+
link_or_text + (divider || '')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Enums
|
2
|
+
|
3
|
+
# Public: Contains all valid options as Fixnum constants for the various ways that potential
|
4
|
+
# duplicate records can be handled.
|
5
|
+
module DuplicateStrategy
|
6
|
+
ALLOW = 0
|
7
|
+
REPLACE = 1
|
8
|
+
SKIP = 2
|
9
|
+
|
10
|
+
ALL_OPTIONS = [ALLOW, REPLACE, SKIP]
|
11
|
+
|
12
|
+
# Public: Will advise if the provided value passed in is valid.
|
13
|
+
# Note: This is a module method, not an 'instance' method, as the generated documentation
|
14
|
+
# might imply.
|
15
|
+
#
|
16
|
+
# value - String value to test for validity.
|
17
|
+
#
|
18
|
+
# Returns Boolean whether the value provided is valid.
|
19
|
+
def valid?(value)
|
20
|
+
ALL_OPTIONS.include? value
|
21
|
+
end
|
22
|
+
|
23
|
+
end # module DuplicateStrategy
|
24
|
+
|
25
|
+
end # module Enums
|
data/lib/enums.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
# This module deals with importing and exporting model data. We are a counterpart
|
2
|
+
# to controllers under the same Interchange module.
|
3
|
+
module Interchange
|
4
|
+
|
5
|
+
# Public: Handles saving model data (exporting) to the local file system, be it intended for
|
6
|
+
# later imports back into Mockingjay, or for use as test data for UIAutomation.
|
7
|
+
class Writer
|
8
|
+
# Mixins
|
9
|
+
include ::Mixins::Logging
|
10
|
+
include ::ActionView::Helpers::TextHelper # for pluralize
|
11
|
+
|
12
|
+
# A Hash of results from the last operation, containing keys:
|
13
|
+
# :successful - Array of model instances whose export was successful.
|
14
|
+
# :failures - Array of model instances whose export was unsuccessful.
|
15
|
+
attr_accessor :operation_results
|
16
|
+
|
17
|
+
|
18
|
+
# Public: This demarcates a new export operation. You must provide a block
|
19
|
+
# for us to yield to when you call this method. That block should
|
20
|
+
# contain one or more calls to our export() method.
|
21
|
+
#
|
22
|
+
# We reset the class variable 'operation_results' and populate it
|
23
|
+
# after yielding, with summary information about the export operation.
|
24
|
+
#
|
25
|
+
# Returns nothing.
|
26
|
+
def mark_export_operation
|
27
|
+
log "Starting Export operation."
|
28
|
+
# Clear the class cached last-export summary:
|
29
|
+
self.class.instance_variable_set(:@last_export_summary, nil)
|
30
|
+
@operation_results = { successful: [], failures: [] }
|
31
|
+
|
32
|
+
yield # Hand over control to the block that calls our export() method one or more times
|
33
|
+
|
34
|
+
log "Completed Export operation."
|
35
|
+
summarize_operation
|
36
|
+
self.class.instance_variable_set(:@last_export_summary, @operation_results)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Public: This inspects the @operation_results Hash and adds more entries to it that describe
|
41
|
+
# and timestamp the operation. Included in this is a 'flash_hash' that a controller can
|
42
|
+
# use after the export operation, to present a one line summary.
|
43
|
+
#
|
44
|
+
# Returns nothing.
|
45
|
+
def summarize_operation
|
46
|
+
message = ''
|
47
|
+
|
48
|
+
# Failures:
|
49
|
+
if @operation_results[:failures].empty?
|
50
|
+
flash_key = :notice
|
51
|
+
@operation_results[:has_failures] = false
|
52
|
+
else
|
53
|
+
flash_key = :alert
|
54
|
+
@operation_results[:has_failures] = true
|
55
|
+
message = "#{@operation_results[:failures].count} #{@model_class.to_s} record(s) failed to export. "
|
56
|
+
end
|
57
|
+
|
58
|
+
# Summary Info:
|
59
|
+
@operation_results[:total_records] = @operation_results[:failures].count + @operation_results[:successful].count
|
60
|
+
message.concat(pluralize(@operation_results[:successful].count, "#{@model_class.to_s} record")).concat(' exported successfully. ')
|
61
|
+
@operation_results[:flash_hash] = { flash_key => message }
|
62
|
+
@operation_results[:timestamp] = Time.now
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# Public: This determines which specific objects will get exported, and then farms that out to the create_file()
|
67
|
+
# method to actually write to the file system.
|
68
|
+
#
|
69
|
+
# We can handle multiple objects or a single object as well as regular exports and those
|
70
|
+
# meant for UIAutomation test data. Most of these options are specified in the optional options hash.
|
71
|
+
# In all cases, each call to this method is for dealing with objects of a specific model class.
|
72
|
+
#
|
73
|
+
# model_class - Class of model to be exported.
|
74
|
+
# options - Hash of options that help us identify what subset of data (instances belonging to the model class)
|
75
|
+
# that we are meant to export. These options can include:
|
76
|
+
# :ids - what ID values to limit the export to (default: nil)
|
77
|
+
# :keywords - what keywords a model must have at least one of, in order to qualify for export (default: nil)
|
78
|
+
def export(model_class, options = {})
|
79
|
+
ids = options[:ids]
|
80
|
+
keywords = options[:keywords]
|
81
|
+
base_path = options[:base_path]
|
82
|
+
|
83
|
+
log "keywords sought: #{keywords}"
|
84
|
+
|
85
|
+
controller_name = "Interchange::#{model_class.to_s.pluralize}Controller"
|
86
|
+
controller = controller_name.constantize
|
87
|
+
@rendering_controller = controller.new
|
88
|
+
|
89
|
+
# Create dummy request and response objects on the rendering controller,
|
90
|
+
# otherwise it will complain. This was gleaned from: http://stackoverflow.com/a/6773022/535054
|
91
|
+
@rendering_controller.request = ActionDispatch::TestRequest.new
|
92
|
+
@rendering_controller.response = ActionDispatch::TestResponse.new
|
93
|
+
|
94
|
+
# Retrieve instances of the given model, and then iterate through them to do the export:
|
95
|
+
models =
|
96
|
+
if ids
|
97
|
+
# We have a specific list of ID values we've been asked to export:
|
98
|
+
model_class.where(:id => ids)
|
99
|
+
elsif keywords
|
100
|
+
# We want only those instances that have one of the list of keywords given to us:
|
101
|
+
model_class.joins(:metadata_keywords).where(:metadata_keywords => {:text => keywords})
|
102
|
+
else
|
103
|
+
# We have no specific list of IDs, so we're going to export all instances:
|
104
|
+
model_class.find(:all)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Make the call to create_file() for each model instance we've determined needs to be exported:
|
108
|
+
models.each do |model|
|
109
|
+
begin
|
110
|
+
# TODO: Ensure this is a top-level resource before exporting
|
111
|
+
create_file model, options
|
112
|
+
@operation_results[:successful] << model
|
113
|
+
rescue Exception => exception
|
114
|
+
@operation_results[:failures] << model
|
115
|
+
model.export_errors << "Failure details: #{exception}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
# Internal: Decorates the passed in pretty JSON if needed. Wwe adorn the
|
122
|
+
# output with JavaScript that provides comments and variable instantiation.
|
123
|
+
#
|
124
|
+
# model - ActiveRecord::Base the root model being exported, from which we can get class
|
125
|
+
# and name information to decorate with.
|
126
|
+
# pretty_json - String with contents ready to be exported, that we'll decorate.
|
127
|
+
# options - Hash of options. (default: {})
|
128
|
+
#
|
129
|
+
# Returns String of the decorated output.
|
130
|
+
def decorate_output(model, pretty_json, options={})
|
131
|
+
decorated_output = "// #{model.interchange_name}.js\n"
|
132
|
+
decorated_output.concat "var CERN#{model.interchange_type_name}_#{model.interchange_name} = #{pretty_json};"
|
133
|
+
|
134
|
+
decorated_output
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# Internal: Creates a local file that contains the provided JSON payload for the given model object.
|
139
|
+
# Model objects of a similar class are co-located in their own sub-directory.
|
140
|
+
#
|
141
|
+
# model - ActiveRecord::Base the root model to be exported.
|
142
|
+
# options - Hash of options. (default: {})
|
143
|
+
#
|
144
|
+
# Returns nothing.
|
145
|
+
def create_file(model, options={})
|
146
|
+
Rails.logger.debug "Performing local export of #{model.class} model: #{model.short_title}"
|
147
|
+
model.export_errors = []
|
148
|
+
pretty_json = @rendering_controller.render_show_template_to_string(model, options)
|
149
|
+
data_file_contents = decorate_output(model, pretty_json, options)
|
150
|
+
base_path = options[:base_path] || 'public/interchange/'
|
151
|
+
|
152
|
+
interchange_extension = 'js'
|
153
|
+
interchange_type = if base_path == 'public/interchange/'
|
154
|
+
'export/'
|
155
|
+
else
|
156
|
+
''
|
157
|
+
end
|
158
|
+
|
159
|
+
# Determine directory we will write the file into:
|
160
|
+
directory = "#{base_path}#{interchange_type}#{model.class.to_s.pluralize}"
|
161
|
+
|
162
|
+
# Ensure the directory exists:
|
163
|
+
directory_absolute_path = File.join(Rails.root, directory)
|
164
|
+
Rails.logger.debug "Absolute directory path to write to: #{directory_absolute_path}"
|
165
|
+
system("mkdir -p #{directory_absolute_path}") unless File.exists?(directory_absolute_path)
|
166
|
+
|
167
|
+
# Write the file to disk:
|
168
|
+
file_absolute_path = "#{directory_absolute_path}/#{model.interchange_name}.#{interchange_extension}"
|
169
|
+
Rails.logger.debug "Absolute model file path to write to: #{file_absolute_path}"
|
170
|
+
file = File.new(file_absolute_path, "w")
|
171
|
+
file.write(data_file_contents)
|
172
|
+
file.close
|
173
|
+
end
|
174
|
+
|
175
|
+
end # class
|
176
|
+
end # module
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# This module deals with importing and exporting model data. We are a counterpart
|
2
|
+
# to controllers under the same Interchange module.
|
3
|
+
module Interchange
|
4
|
+
|
5
|
+
# Handles reading files from the local file system meant for import. Ultimately, we'll pass a JSON object
|
6
|
+
# to the model whose data we've loaded, and that model will be responsible for creating a new instance of itself
|
7
|
+
# and all of its constituent, wholly owned, related objects.
|
8
|
+
#
|
9
|
+
# You use mark_import_operation() with a block in which you call import() one or more times, to perform your
|
10
|
+
# import operation.
|
11
|
+
class Reader
|
12
|
+
# Mixins
|
13
|
+
include ::Mixins::Logging
|
14
|
+
include ::ActionView::Helpers::TextHelper # for pluralize
|
15
|
+
|
16
|
+
attr_accessor :operation_results
|
17
|
+
|
18
|
+
# Prepares for an import operation and cleans up after it, summarizing the results.
|
19
|
+
# Callers must provide a block that makes one or more calls to our import() method to import
|
20
|
+
# various models.
|
21
|
+
def mark_import_operation
|
22
|
+
# Clear the class cached last-import summary:
|
23
|
+
self.class.instance_variable_set(:@last_import_summary, nil)
|
24
|
+
|
25
|
+
# Create a hash we'll return as the result of the import
|
26
|
+
@operation_results = { successful: [], failures: [], skipped: [] }
|
27
|
+
|
28
|
+
yield # Hand over control to the block that calls our import() method one or more times
|
29
|
+
|
30
|
+
# Now mark the operation as complete:
|
31
|
+
summarize_operation
|
32
|
+
self.class.instance_variable_set(:@last_import_summary, @operation_results)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# We kick off the import process for all records in the import folder that belong to the provided model class.
|
37
|
+
# If not provided, we default to no clean up of files and the skipping of any duplicates.
|
38
|
+
#
|
39
|
+
# @param model_class The actual top-level model class whose json files we'll look for and import.
|
40
|
+
# @param cleanup Defaults to false. If true, this instructs the successful import operations to be
|
41
|
+
# followed up with a deletion of the source file for import.
|
42
|
+
# @param duplicate_strategy Defaults to skipping duplicates. Provide a value in module Enums::DuplicateStrategy.
|
43
|
+
# @return A hash that summarizes the operation. Included within is another hash that can be used for the Flash
|
44
|
+
# that provides an alert or notice summary, as appropriate.
|
45
|
+
def import(model_class, cleanup=false, duplicate_strategy=Enums::DuplicateStrategy::SKIP, base_path='public/interchange/import/')
|
46
|
+
log "Importing files for model class: #{model_class}, with options" \
|
47
|
+
"cleanup: #{cleanup}, duplicate_strategy: #{duplicate_strategy}, base_path: #{base_path}"
|
48
|
+
|
49
|
+
@model_class = model_class
|
50
|
+
@cleanup_requested = cleanup
|
51
|
+
|
52
|
+
log "Started import for model: #{@model_class.to_s}"
|
53
|
+
|
54
|
+
# Determine directory we will read files from:
|
55
|
+
directory = "#{base_path}#{@model_class.to_s.pluralize}"
|
56
|
+
|
57
|
+
# Ensure the directory exists:
|
58
|
+
models_path = File.join(Rails.root, directory)
|
59
|
+
log "Models path to read from: #{models_path}"
|
60
|
+
|
61
|
+
# Does the directory exist that files of the given model class would be found at?
|
62
|
+
if File.exists?(models_path)
|
63
|
+
log "Import path '#{models_path}' exists."
|
64
|
+
Dir.chdir models_path
|
65
|
+
filenames = Dir.glob("*.{js,json}") # The candidate files to import will all have a .json or .js extension.
|
66
|
+
log "Matched the following file paths: #{filenames}"
|
67
|
+
|
68
|
+
# Iterate through all the files in the model directory, and attempt to import each one:
|
69
|
+
filenames.each do | filename |
|
70
|
+
log "#{self.class.name} import(): Processing file '#{filename}'"
|
71
|
+
contents = File.read(filename)
|
72
|
+
massaged_contents = transform_javascript_format(contents)
|
73
|
+
model = @model_class.send :import, massaged_contents, duplicate_strategy
|
74
|
+
|
75
|
+
if model.skip_import?
|
76
|
+
log "#{self.class.name} import(): Skipping import of file '#{filename}'"
|
77
|
+
process_skipped_import(model, filename)
|
78
|
+
elsif model.import_errors.empty?
|
79
|
+
log "#{self.class.name} import(): Success at import of file '#{filename}'"
|
80
|
+
process_model_save(model, filename)
|
81
|
+
else
|
82
|
+
log "#{self.class.name} import(): Import failed for file '#{filename}'"
|
83
|
+
process_failed_import(model)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
else
|
87
|
+
log "No import folder exists at path: #{models_path}"
|
88
|
+
end
|
89
|
+
|
90
|
+
end # import
|
91
|
+
|
92
|
+
|
93
|
+
# This takes a file with camel case keys and makes them underscore keys, recognizable
|
94
|
+
# to our Ruby models. Additionally, JavaScript commands that encase the core payload,
|
95
|
+
# are stripped out, so that we have valid JSON we can parse.
|
96
|
+
#
|
97
|
+
# Doing this whole exercise lets us export to JavaScript files useful for UIAutomation,
|
98
|
+
# while having a single format we can also import for ourselves (Mockingjay).
|
99
|
+
def transform_javascript_format(contents)
|
100
|
+
# Find positions for the start and end braces:
|
101
|
+
start_position = contents.index('{')
|
102
|
+
end_position = contents.rindex('}')
|
103
|
+
|
104
|
+
# Retrieve the string contents of the file between these braces, inclusive:
|
105
|
+
massaged_contents = contents[start_position..end_position]
|
106
|
+
|
107
|
+
# Parse the contents into JSON, and the make all the keys underscore style:
|
108
|
+
JSON.parse(massaged_contents).underscore_keys
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
# Records that the import attempt for the given model, failed. We don't delete the source file in this case,
|
115
|
+
# regardless of the value of @cleanup_requested, because the user would likely want to inspect the file,
|
116
|
+
# retry after adjustments, etc.
|
117
|
+
#
|
118
|
+
# @param model The model instance that failed to import. This should have at least one entry in its
|
119
|
+
# import_errors array.
|
120
|
+
def process_failed_import(model)
|
121
|
+
@operation_results[:failures] << model
|
122
|
+
log "Reader.import(): ERROR. Could not save imported model #{model.short_title}." \
|
123
|
+
"Errors: #{model.errors.full_messages.join(' | ')}"
|
124
|
+
# Note: for failures, we obviously do not want to delete the source file; so that it might be inspected.
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
# Attempts to save the provided model. Records a successful import if it succeeds, and a failure if it doesn't.
|
129
|
+
# @param model The model instance we will attempt to save.
|
130
|
+
# @param filename The source filename (fully qualified) that the model was built from. We'll use this to know
|
131
|
+
# what file to delete if the save was successful and cleanup was requested.
|
132
|
+
def process_model_save(model, filename)
|
133
|
+
log "#{self.class.name} process_model_save(): Entered method."
|
134
|
+
begin
|
135
|
+
if model.valid?
|
136
|
+
log "#{self.class.name} process_model_save(): model is: #{model}"
|
137
|
+
#Rails.application.eager_load! # Needed to ensure all models are loaded for our asking them if they are root resources.
|
138
|
+
model.save
|
139
|
+
@operation_results[:successful] << model
|
140
|
+
log "Reader.import(): Successfully saved imported model #{model.model_qualified_title}"
|
141
|
+
# Delete the source file, if cleanup was requested:
|
142
|
+
File.delete(filename) if @cleanup_requested
|
143
|
+
else
|
144
|
+
@operation_results[:failures] << model
|
145
|
+
log_error "Reader.import(): Error saving model #{model.model_qualified_title}. Details: #{model.errors.full_messages}"
|
146
|
+
model.import_errors << "Save failed with error: #{model.errors.full_messages}"
|
147
|
+
end
|
148
|
+
rescue Exception => e
|
149
|
+
log "#{self.class.name} process_model_save(): Exception: #{e}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Records that we skipped importing a model, because it was a duplicate.
|
155
|
+
# @param model The model instance that we have skipped an import of, because it would duplicate one or more
|
156
|
+
# records already in the system.
|
157
|
+
# @param filename The source filename (fully qualified) that the model was built from. We'll use this to know
|
158
|
+
# what file to delete if cleanup was requested.
|
159
|
+
def process_skipped_import(model, filename)
|
160
|
+
@operation_results[:skipped] << model
|
161
|
+
log "Reader.import(): Skipped importing model #{model.model_qualified_title}"
|
162
|
+
# Delete the source file, if cleanup was requested:
|
163
|
+
File.delete(filename) if @cleanup_requested
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# Adorns the summary hash passed in, beyond its core arrays of successful model import and failed model imports,
|
168
|
+
# with keys describing other aspects of the operation, including:
|
169
|
+
# * has_failures => Boolean on whether any failures exist
|
170
|
+
# * total_records => Sum total of successful and failed model imports; the total count of models we attempted to import
|
171
|
+
# * flash_hash => A hash that can be used for the flash messaging that summarizes the status of this import
|
172
|
+
def summarize_operation
|
173
|
+
message = ''
|
174
|
+
|
175
|
+
# Failures:
|
176
|
+
if @operation_results[:failures].empty?
|
177
|
+
flash_key = :notice
|
178
|
+
@operation_results[:has_failures] = false
|
179
|
+
else
|
180
|
+
flash_key = :alert
|
181
|
+
@operation_results[:has_failures] = true
|
182
|
+
message = "#{@operation_results[:failures].count} record(s) failed to import. "
|
183
|
+
end
|
184
|
+
|
185
|
+
# Skipped:
|
186
|
+
@operation_results[:has_skips] = !@operation_results[:skipped].empty?
|
187
|
+
|
188
|
+
# Summary Info:
|
189
|
+
@operation_results[:total_records] = @operation_results[:failures].count + @operation_results[:successful].count + @operation_results[:skipped].count
|
190
|
+
message.concat(pluralize(@operation_results[:successful].count, "record")).concat(' imported successfully. ')
|
191
|
+
message.concat(pluralize(@operation_results[:skipped].count, "record")).concat(' skipped. ')
|
192
|
+
@operation_results[:flash_hash] = { flash_key => message }
|
193
|
+
@operation_results[:timestamp] = Time.now
|
194
|
+
end
|
195
|
+
|
196
|
+
end # class
|
197
|
+
end # module
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Mixins::Logging
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
end
|
7
|
+
|
8
|
+
# ---------- Instance Methods ----------
|
9
|
+
|
10
|
+
# Convenience wrapper for Rails.logger.debug
|
11
|
+
def log(message)
|
12
|
+
log_debug message
|
13
|
+
end
|
14
|
+
|
15
|
+
# An alias for the more abbreviated log() method,
|
16
|
+
# for consistency.
|
17
|
+
def log_debug(message)
|
18
|
+
Rails.logger.debug message
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def log_info(message)
|
23
|
+
Rails.logger.info message
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def log_warn(message)
|
28
|
+
Rails.logger.warn message
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def log_error(message)
|
33
|
+
Rails.logger.error message
|
34
|
+
end
|
35
|
+
|
36
|
+
end # module Logging
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Public: This is where you invoke the load command for each reference data type that piggybacks off of the
|
2
|
+
# DataList and DataOption models.
|
3
|
+
#
|
4
|
+
# Any reference data model with nested reference data structure, is responsible for loading those nested
|
5
|
+
# data lists and options.
|
6
|
+
#
|
7
|
+
# Relies on the constant ROOT_REFERENCE_MODEL_CLASSES, defined in config/initializers/reference_data_types
|
8
|
+
class Reference::DataTypes
|
9
|
+
|
10
|
+
# Loads all reference data classes with information from DataSets marked active.
|
11
|
+
def self.load
|
12
|
+
Rails.logger.info "Reference::DataTypes load(): Loading reference data types."
|
13
|
+
Reference::ROOT_REFERENCE_MODEL_CLASSES.each { |reference_class| reference_class.send(:load_reference_data)}
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# De-registers all cached instances of reference data from each reference data type.
|
18
|
+
def self.unload
|
19
|
+
Rails.logger.info "Reference::DataTypes unload(): Unloading reference data types."
|
20
|
+
Reference::ROOT_REFERENCE_MODEL_CLASSES.each { |reference_class| reference_class.send(:unload_reference_data)}
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Reloads our reference data. Handy if a data list or data set has changed.
|
25
|
+
def self.reload
|
26
|
+
Rails.logger.info "Reference::DataTypes reload(): Reloading reference data types."
|
27
|
+
self.unload
|
28
|
+
self.load
|
29
|
+
end
|
30
|
+
|
31
|
+
end # class
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Reference
|
2
|
+
# Public: This class handles the import of data located in the 'data/reference/' folder.
|
3
|
+
#
|
4
|
+
# We seed the database with default values for reference data.
|
5
|
+
class Loader
|
6
|
+
|
7
|
+
# Mixins
|
8
|
+
include ::Mixins::Logging
|
9
|
+
|
10
|
+
# Internal: Creates our instance of Reader, that handles the actual import.
|
11
|
+
def initialize
|
12
|
+
@reader = Interchange::Reader.new
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# Public: The primary interface to this class. We setup to load all reference data in the
|
17
|
+
# data/reference/ folder, such that we leave the seed files intact, do not destroy existing data prior,
|
18
|
+
# and that we just replace any imports for data that already exists based on each model's determination
|
19
|
+
# of uniqueness.
|
20
|
+
def perform_import
|
21
|
+
log "#{self.class.name} perform_import(): Began import process."
|
22
|
+
# Set the parameters guiding how the import should be run:
|
23
|
+
cleanup = false
|
24
|
+
duplicate_strategy = Enums::DuplicateStrategy::REPLACE
|
25
|
+
|
26
|
+
# Wrap the import process in a block that can log the overall status:
|
27
|
+
@reader.mark_import_operation do
|
28
|
+
# Kick off the underlying import operation on the model(s) requested:
|
29
|
+
root_model_classes.each do |model_class|
|
30
|
+
puts "Reference::Loader - Loading reference data (if any) for model: #{model_class.to_s}"
|
31
|
+
@reader.import model_class, cleanup, duplicate_strategy, Reference::BASE_PATH
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Determines which of our models are root resources. Only these attempt to get loaded.
|
40
|
+
def root_model_classes
|
41
|
+
[DataSet]
|
42
|
+
end
|
43
|
+
|
44
|
+
end # class
|
45
|
+
|
46
|
+
end # module
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Reference
|
2
|
+
|
3
|
+
# Public: Writes reference data back to the file system, which will automatically get loaded next time.
|
4
|
+
# This will also get flagged in source control as an update that others will receive, who are running locally.
|
5
|
+
class Updater
|
6
|
+
|
7
|
+
# This constructor sets up Writer class that we offload the low level export work to.
|
8
|
+
def initialize
|
9
|
+
@writer = Interchange::Writer.new
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def perform_export
|
14
|
+
# Start the export operation block:
|
15
|
+
@writer.mark_export_operation do
|
16
|
+
# Kick off the underlying export operation on the reference data model requested:
|
17
|
+
@writer.export DataSet, base_path: Reference::BASE_PATH, suppress_database_ids: true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end # class: Updater
|
22
|
+
|
23
|
+
end # module: Reference
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# This helper class is meant to get yielded to a block of code in a show template.
|
2
|
+
# We hold the model object whose attributes are to be displayed, and handle substituting
|
3
|
+
# values into a partial template, abstracting HTML boilerplate from the resource's
|
4
|
+
# show template itself.
|
5
|
+
#
|
6
|
+
# We are very similar to the form that gets yielded by a form_for / simple_form_for call
|
7
|
+
# except that we are making a show template easier to build, instead of a form.
|
8
|
+
#
|
9
|
+
class ResourceDisplayHelper
|
10
|
+
|
11
|
+
# Attributes
|
12
|
+
attr_accessor :model_object
|
13
|
+
attr_accessor :resource_helper
|
14
|
+
|
15
|
+
|
16
|
+
# ---------- Initializers ----------
|
17
|
+
|
18
|
+
# Provide the model object we'll be invoking attribute method calls on, as well as the
|
19
|
+
# resource helper that we can ask to render a view partial for us.
|
20
|
+
def initialize(model_object, resource_helper)
|
21
|
+
@model_object = model_object
|
22
|
+
@resource_helper = resource_helper
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# ---------- Interface ----------
|
27
|
+
|
28
|
+
# This is the main method of this class that callers will use. They provide
|
29
|
+
# the attribute whose label and value is to be displayed. Optional overrides
|
30
|
+
# for the label and value are also allowed.
|
31
|
+
def attribute(name, options={})
|
32
|
+
label = options[:label] || name.to_s.humanize.titleize
|
33
|
+
value = options[:value] || model_object.send(name)
|
34
|
+
link_path = options[:link_path]
|
35
|
+
|
36
|
+
if value && value.kind_of?(String) && options[:truncate]
|
37
|
+
value = value.truncate(options[:truncate], :omission => "...", :separator => ' ')
|
38
|
+
end
|
39
|
+
|
40
|
+
# We don't have access to the view renderer, but our provider resource helper does.
|
41
|
+
# Use it to render the partial that holds the boilerplate template for how we like
|
42
|
+
# to show a single attribute:
|
43
|
+
resource_helper.render partial: 'mock/common/show_attribute', locals: {label: label, value: value, link_path: link_path}
|
44
|
+
end
|
45
|
+
|
46
|
+
end # class
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# NOTE: only doing this in development as some production environments (Heroku)
|
2
|
+
# NOTE: are sensitive to local FS writes, and besides -- it's just not proper
|
3
|
+
# NOTE: to have a dev-mode tool do its thing in production.
|
4
|
+
if(Rails.env.development?)
|
5
|
+
task :set_annotation_options do
|
6
|
+
ENV['position_in_class'] = "before"
|
7
|
+
ENV['position_in_fixture'] = "before"
|
8
|
+
ENV['position_in_factory'] = "before"
|
9
|
+
ENV['show_indexes'] = "true"
|
10
|
+
ENV['include_version'] = "false"
|
11
|
+
ENV['exclude_tests'] = "false"
|
12
|
+
ENV['exclude_fixtures'] = "false"
|
13
|
+
ENV['ignore_model_sub_dir'] = "false"
|
14
|
+
ENV['skip_on_db_migrate'] = "false"
|
15
|
+
ENV['format_rdoc'] = "false"
|
16
|
+
ENV['format_markdown'] = "false"
|
17
|
+
ENV['no_sort'] = "false"
|
18
|
+
ENV['force'] = "false"
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ion_orders_engine_mockingjay
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Schuster
|
@@ -120,6 +120,10 @@ files:
|
|
120
120
|
- app/helpers/reference/data_elements_helper.rb
|
121
121
|
- app/helpers/reference/data_lists_helper.rb
|
122
122
|
- app/helpers/reference/data_sets_helper.rb
|
123
|
+
- app/models/concerns/mock/import_concern.rb
|
124
|
+
- app/models/concerns/mock/phone_number_concern.rb
|
125
|
+
- app/models/concerns/mock/resource_ancestors_concern.rb
|
126
|
+
- app/models/concerns/mock/root_model_concern.rb
|
123
127
|
- app/views/interchange/common/_database_info.json.jbuilder
|
124
128
|
- app/views/interchange/common/_metadata_keywords.json.jbuilder
|
125
129
|
- app/views/interchange/common/_test_assertions.json.jbuilder
|
@@ -180,11 +184,23 @@ files:
|
|
180
184
|
- config/locales/simple_form.en.yml
|
181
185
|
- config/mongo.yml
|
182
186
|
- config/routes.rb
|
187
|
+
- db/development.sqlite3
|
183
188
|
- db/schema.rb
|
184
189
|
- db/seeds.rb
|
190
|
+
- lib/bootstrap_breadcrumbs_builder.rb
|
191
|
+
- lib/enums/duplicate_strategy.rb
|
192
|
+
- lib/enums.rb
|
193
|
+
- lib/interchange/reader.rb
|
194
|
+
- lib/interchange/Writer.rb
|
185
195
|
- lib/ion_orders_engine_mockingjay/engine.rb
|
186
196
|
- lib/ion_orders_engine_mockingjay/version.rb
|
187
197
|
- lib/ion_orders_engine_mockingjay.rb
|
198
|
+
- lib/mixins/logging.rb
|
199
|
+
- lib/reference/data_types.rb
|
200
|
+
- lib/reference/loader.rb
|
201
|
+
- lib/reference/updater.rb
|
202
|
+
- lib/resource_display_helper.rb
|
203
|
+
- lib/tasks/auto_annotate_models.rake
|
188
204
|
- lib/tasks/ion_orders_engine_mockingjay_tasks.rake
|
189
205
|
- Rakefile
|
190
206
|
- README.md
|
@@ -202,9 +218,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
202
218
|
version: '0'
|
203
219
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
204
220
|
requirements:
|
205
|
-
- - '
|
221
|
+
- - '>='
|
206
222
|
- !ruby/object:Gem::Version
|
207
|
-
version:
|
223
|
+
version: '0'
|
208
224
|
requirements: []
|
209
225
|
rubyforge_project:
|
210
226
|
rubygems_version: 2.1.4
|