ion_orders_engine_mockingjay 1.0.1.SNAPSHOT → 1.0.2
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.
- 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
|