lims-core 3.2.3
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 +15 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rvmrc +2 -0
- data/.travis.yml +2 -0
- data/.vimrc +27 -0
- data/.yard_templates/default/layout/html/footer.erb +0 -0
- data/.yardopts +1 -0
- data/Gemfile +54 -0
- data/Gemfile.lock +197 -0
- data/Guardfile +21 -0
- data/Guardfile.tmux +28 -0
- data/README.markdown +67 -0
- data/Rakefile +16 -0
- data/config/database.yml +16 -0
- data/doc/Array.html +116 -0
- data/doc/Array/ArrayLoggerPersistor.html +152 -0
- data/doc/Lims.html +114 -0
- data/doc/Lims/Core.html +178 -0
- data/doc/Lims/Core/Action.html +91 -0
- data/doc/Lims/Core/Actions.html +116 -0
- data/doc/Lims/Core/Actions/Action.html +216 -0
- data/doc/Lims/Core/Actions/Action/AfterEval.html +853 -0
- data/doc/Lims/Core/Actions/Action/InvalidParameters.html +268 -0
- data/doc/Lims/Core/Actions/ActionGroup.html +196 -0
- data/doc/Lims/Core/Actions/ActionGroup/AfterEval.html +315 -0
- data/doc/Lims/Core/Actions/BulkAction.html +224 -0
- data/doc/Lims/Core/Actions/BulkAction/AfterEval.html +253 -0
- data/doc/Lims/Core/Actions/TestActionGroup.html +101 -0
- data/doc/Lims/Core/Actions/TestActionGroup/Action.html +133 -0
- data/doc/Lims/Core/Actions/TestActionGroup/ActionGroup.html +127 -0
- data/doc/Lims/Core/Base.html +287 -0
- data/doc/Lims/Core/Base/AccessibleViaSuper.html +252 -0
- data/doc/Lims/Core/Base/ClassMethod.html +177 -0
- data/doc/Lims/Core/Base/HashString.html +177 -0
- data/doc/Lims/Core/Base/IsArrayOf.html +606 -0
- data/doc/Lims/Core/Base/State.html +130 -0
- data/doc/Lims/Core/Organization.html +113 -0
- data/doc/Lims/Core/Organization/Batch.html +106 -0
- data/doc/Lims/Core/Persistence.html +267 -0
- data/doc/Lims/Core/Persistence/ComparisonFilter.html +318 -0
- data/doc/Lims/Core/Persistence/Filter.html +252 -0
- data/doc/Lims/Core/Persistence/IdentityMap.html +409 -0
- data/doc/Lims/Core/Persistence/IdentityMap/Class.html +144 -0
- data/doc/Lims/Core/Persistence/IdentityMap/DuplicateError.html +126 -0
- data/doc/Lims/Core/Persistence/IdentityMap/DuplicateIdError.html +136 -0
- data/doc/Lims/Core/Persistence/IdentityMap/DuplicateObjectError.html +136 -0
- data/doc/Lims/Core/Persistence/IdentityMapClass.html +133 -0
- data/doc/Lims/Core/Persistence/Logger.html +105 -0
- data/doc/Lims/Core/Persistence/Logger/Persistor.html +334 -0
- data/doc/Lims/Core/Persistence/Logger/Session.html +452 -0
- data/doc/Lims/Core/Persistence/Logger/Store.html +470 -0
- data/doc/Lims/Core/Persistence/MessageBus.html +871 -0
- data/doc/Lims/Core/Persistence/MessageBus/ConnectionError.html +123 -0
- data/doc/Lims/Core/Persistence/MessageBus/InvalidSettingsError.html +122 -0
- data/doc/Lims/Core/Persistence/MultiCriteriaFilter.html +293 -0
- data/doc/Lims/Core/Persistence/PersistAssociationTrait.html +91 -0
- data/doc/Lims/Core/Persistence/PersistableTrait.html +91 -0
- data/doc/Lims/Core/Persistence/Persistor.html +3072 -0
- data/doc/Lims/Core/Persistence/Persistor/DuplicateError.html +205 -0
- data/doc/Lims/Core/Persistence/Persistor/DuplicateIdError.html +147 -0
- data/doc/Lims/Core/Persistence/Persistor/DuplicateObjectError.html +147 -0
- data/doc/Lims/Core/Persistence/PersistorTrait.html +91 -0
- data/doc/Lims/Core/Persistence/ResourceState.html +1738 -0
- data/doc/Lims/Core/Persistence/Search.html +269 -0
- data/doc/Lims/Core/Persistence/Search/CreateSearch.html +251 -0
- data/doc/Lims/Core/Persistence/Search/SearchPersistor.html +240 -0
- data/doc/Lims/Core/Persistence/Search/SearchSequelPersistor.html +396 -0
- data/doc/Lims/Core/Persistence/Sequel.html +117 -0
- data/doc/Lims/Core/Persistence/Sequel/Filters.html +462 -0
- data/doc/Lims/Core/Persistence/Sequel/ForTest.html +101 -0
- data/doc/Lims/Core/Persistence/Sequel/ForTest/Name.html +137 -0
- data/doc/Lims/Core/Persistence/Sequel/ForTest/Name/NamePersitor.html +143 -0
- data/doc/Lims/Core/Persistence/Sequel/Migrations.html +266 -0
- data/doc/Lims/Core/Persistence/Sequel/Persistor.html +665 -0
- data/doc/Lims/Core/Persistence/Sequel/Session.html +501 -0
- data/doc/Lims/Core/Persistence/Sequel/Store.html +417 -0
- data/doc/Lims/Core/Persistence/Session.html +2751 -0
- data/doc/Lims/Core/Persistence/Session/UnmanagedObjectError.html +111 -0
- data/doc/Lims/Core/Persistence/StateGroup.html +696 -0
- data/doc/Lims/Core/Persistence/StateList.html +498 -0
- data/doc/Lims/Core/Persistence/Store.html +695 -0
- data/doc/Lims/Core/Persistence/UuidResource.html +1044 -0
- data/doc/Lims/Core/Persistence/UuidResource/InvalidUuidError.html +111 -0
- data/doc/Lims/Core/Persistence/UuidResource/UuidResourcePersistor.html +337 -0
- data/doc/Lims/Core/Persistence/Uuidable.html +799 -0
- data/doc/Lims/Core/Persistor.html +320 -0
- data/doc/Lims/Core/Resource.html +165 -0
- data/doc/Object.html +228 -0
- data/doc/SessionSpec.html +101 -0
- data/doc/SessionSpec/Model.html +279 -0
- data/doc/SessionSpec/Model/ModelPersistor.html +327 -0
- data/doc/_index.html +732 -0
- data/doc/class_list.html +47 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +55 -0
- data/doc/css/style.css +322 -0
- data/doc/file.README.html +127 -0
- data/doc/file_list.html +49 -0
- data/doc/frames.html +13 -0
- data/doc/index.html +127 -0
- data/doc/js/app.js +205 -0
- data/doc/js/full_list.js +167 -0
- data/doc/js/jquery.js +16 -0
- data/doc/method_list.html +1894 -0
- data/doc/top-level-namespace.html +100 -0
- data/lib/common.rb +18 -0
- data/lib/lims-core.rb +29 -0
- data/lib/lims-core/actions.rb +10 -0
- data/lib/lims-core/actions/action.rb +185 -0
- data/lib/lims-core/actions/action_group.rb +54 -0
- data/lib/lims-core/actions/bulk_action.rb +65 -0
- data/lib/lims-core/base.rb +132 -0
- data/lib/lims-core/helpers.rb +41 -0
- data/lib/lims-core/persistence.rb +15 -0
- data/lib/lims-core/persistence/comparison_filter.rb +54 -0
- data/lib/lims-core/persistence/filter.rb +23 -0
- data/lib/lims-core/persistence/identity_map.rb +55 -0
- data/lib/lims-core/persistence/logger/all.rb +5 -0
- data/lib/lims-core/persistence/logger/persistor.rb +35 -0
- data/lib/lims-core/persistence/logger/session.rb +30 -0
- data/lib/lims-core/persistence/logger/store.rb +37 -0
- data/lib/lims-core/persistence/message_bus.rb +131 -0
- data/lib/lims-core/persistence/multi_criteria_filter.rb +50 -0
- data/lib/lims-core/persistence/persist_association_trait.rb +96 -0
- data/lib/lims-core/persistence/persistable_trait.rb +150 -0
- data/lib/lims-core/persistence/persistor.rb +495 -0
- data/lib/lims-core/persistence/resource_state.rb +157 -0
- data/lib/lims-core/persistence/search.rb +3 -0
- data/lib/lims-core/persistence/search/all.rb +3 -0
- data/lib/lims-core/persistence/search/create_search.rb +55 -0
- data/lib/lims-core/persistence/search/search_persistor.rb +45 -0
- data/lib/lims-core/persistence/search/search_sequel_persistor.rb +40 -0
- data/lib/lims-core/persistence/sequel.rb +14 -0
- data/lib/lims-core/persistence/sequel/filters.rb +106 -0
- data/lib/lims-core/persistence/sequel/migrations.rb +14 -0
- data/lib/lims-core/persistence/sequel/migrations/add_audit_tables.rb +147 -0
- data/lib/lims-core/persistence/sequel/migrations/initial.rb +156 -0
- data/lib/lims-core/persistence/sequel/persistor.rb +200 -0
- data/lib/lims-core/persistence/sequel/session.rb +136 -0
- data/lib/lims-core/persistence/sequel/store.rb +37 -0
- data/lib/lims-core/persistence/session.rb +409 -0
- data/lib/lims-core/persistence/state_group.rb +97 -0
- data/lib/lims-core/persistence/state_list.rb +56 -0
- data/lib/lims-core/persistence/store.rb +73 -0
- data/lib/lims-core/persistence/uuid_resource.rb +115 -0
- data/lib/lims-core/persistence/uuid_resource_persistor.rb +43 -0
- data/lib/lims-core/persistence/uuidable.rb +107 -0
- data/lib/lims-core/resource.rb +21 -0
- data/lib/lims-core/subclass_tracker.rb +30 -0
- data/lib/lims-core/version.rb +5 -0
- data/lims-core.gemspec +40 -0
- data/makefile +52 -0
- data/showoff/core-2012-06-11/core/01_slide.md +237 -0
- data/showoff/core-2012-06-11/core/02_slide.md +110 -0
- data/showoff/core-2012-06-11/custom.css +44 -0
- data/showoff/core-2012-06-11/main/01_slide.md +53 -0
- data/showoff/core-2012-06-11/showoff.json +10 -0
- data/showoff/core-2012-06-11/tp1.tpl +1 -0
- data/spec/actions/action_group_spec.rb +39 -0
- data/spec/actions/spec_helper.rb +1 -0
- data/spec/persistence/identity_map_spec.rb +55 -0
- data/spec/persistence/logger/spec_helper.rb +7 -0
- data/spec/persistence/logger/store_spec.rb +48 -0
- data/spec/persistence/message_bus_spec.rb +76 -0
- data/spec/persistence/sequel/session_spec.rb +125 -0
- data/spec/persistence/sequel/spec_helper.rb +39 -0
- data/spec/persistence/sequel/store_shared.rb +25 -0
- data/spec/persistence/sequel/store_spec.rb +22 -0
- data/spec/persistence/session_spec.rb +199 -0
- data/spec/persistence/spec_helper.rb +2 -0
- data/spec/persistence/uuid_resource_spec.rb +80 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/subclass_tracker_sperc.rb +62 -0
- data/utils/constant_tree.rb +29 -0
- data/utils/stack.rb +48 -0
- metadata +402 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# vi: ts=2:sts=2:et:sw=2 spell:spelllang=en
|
|
2
|
+
|
|
3
|
+
require 'lims-core/persistence/filter'
|
|
4
|
+
require 'lims-core/resource'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
module Lims::Core
|
|
8
|
+
module Persistence
|
|
9
|
+
# Filter performing a && between all the pairs of a map.
|
|
10
|
+
# Key being the field
|
|
11
|
+
# Value can be either a String, an Array or a Hash.
|
|
12
|
+
# Strings and Arrays are normal filters, whereas Hashes
|
|
13
|
+
# correspond to a joined search. The criteria will apply to the
|
|
14
|
+
# joined object corresponding to the key.
|
|
15
|
+
# @example
|
|
16
|
+
# {
|
|
17
|
+
# :status => [:pending, :in_progress],
|
|
18
|
+
# :item => {
|
|
19
|
+
# :status => [:pending],
|
|
20
|
+
# :uuid => <plate_uuid>
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
# Will look for all the orders in pending or in progress status
|
|
24
|
+
# *holding* a plate with a pending status.
|
|
25
|
+
#
|
|
26
|
+
class MultiCriteriaFilter < Filter
|
|
27
|
+
include Resource
|
|
28
|
+
attribute :criteria, Hash, :required => true
|
|
29
|
+
# For Sequel, keys needs to be a Symbol to be seen as column.
|
|
30
|
+
# String are seen as 'value'
|
|
31
|
+
def initialize(criteria)
|
|
32
|
+
criteria = { :criteria => criteria } unless criteria.include?(:criteria)
|
|
33
|
+
criteria[:criteria].rekey!{ |k| k.to_sym }
|
|
34
|
+
super(criteria)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def call(persistor)
|
|
38
|
+
persistor.multi_criteria_filter(criteria)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Persistor
|
|
44
|
+
# @param [Hash] criteria a
|
|
45
|
+
# @return [Persistor]
|
|
46
|
+
def multi_criteria_filter(criteria)
|
|
47
|
+
raise NotImplementedError "multi_criteria_filter methods needs to be implemented for subclass of Persistor"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require 'lims-core/persistence/persistor'
|
|
2
|
+
require 'lims-core/persistence/persistable_trait'
|
|
3
|
+
require 'modularity'
|
|
4
|
+
|
|
5
|
+
module Lims::Core
|
|
6
|
+
module Persistence
|
|
7
|
+
module PersistAssociationTrait
|
|
8
|
+
as_trait do |parent_class=nil, args={}|
|
|
9
|
+
model_name = name.split('::').last.snakecase
|
|
10
|
+
session_name = "#{model_name}_persistor"
|
|
11
|
+
parents = attributes.select { |a| a.options[:relation] == :parent }
|
|
12
|
+
children = attributes.select { |a| a.options[:relation] == :child}
|
|
13
|
+
parent_class.class_eval <<-EOC
|
|
14
|
+
def #{model_name}
|
|
15
|
+
@session.#{session_name}
|
|
16
|
+
end
|
|
17
|
+
EOC
|
|
18
|
+
class_eval <<-EOC
|
|
19
|
+
NOT_IN_ROOT = true
|
|
20
|
+
SESSION_NAME = '#{session_name}'
|
|
21
|
+
def initialize(*args)
|
|
22
|
+
#{
|
|
23
|
+
attributes.map do |att|
|
|
24
|
+
"@#{att.name}=args.shift"
|
|
25
|
+
end.join(';')
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# inline attributes method
|
|
30
|
+
def attributes
|
|
31
|
+
{
|
|
32
|
+
#{
|
|
33
|
+
attributes.map { |a| "#{a.name}: @#{a.name}" }.join(',')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def keys
|
|
39
|
+
[#{
|
|
40
|
+
attributes.reject { |a| a.options[:exclude_from_key] }.map do |a|
|
|
41
|
+
a.options[:primitive].ancestors.include?(Resource) ? "@#{a.name}.object_id" : "@#{a.name}"
|
|
42
|
+
end.join(', ')
|
|
43
|
+
}]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def hash
|
|
47
|
+
keys.hash
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def eql?(other)
|
|
51
|
+
keys == other.keys
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
does 'lims/core/persistence/persistable', :parents => [
|
|
55
|
+
#{
|
|
56
|
+
parents.map do |a|
|
|
57
|
+
a.options.merge(:name => a.name).inspect
|
|
58
|
+
end.join(', ')
|
|
59
|
+
}
|
|
60
|
+
], :children => [
|
|
61
|
+
#{
|
|
62
|
+
children.map { |a| ":#{a.name}" }.join(', ')
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
class #{name.split('::').last}Persistor
|
|
67
|
+
def new_from_attributes(attributes)
|
|
68
|
+
#{
|
|
69
|
+
attributes.map do |a|
|
|
70
|
+
if parents.include?(a)
|
|
71
|
+
"@session_#{a.name} ||= @session.#{a.name}"
|
|
72
|
+
end
|
|
73
|
+
end.join('; ')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
super(attributes) do
|
|
77
|
+
model.new(
|
|
78
|
+
#{
|
|
79
|
+
attributes.map do |a|
|
|
80
|
+
if parents.include?(a)
|
|
81
|
+
"@session_#{a.name}[attributes.delete(:#{a.name}_id)]"
|
|
82
|
+
else
|
|
83
|
+
"attributes.delete(:#{a.name})"
|
|
84
|
+
end
|
|
85
|
+
end.join(', ')
|
|
86
|
+
}
|
|
87
|
+
).tap { |m| m.on_load}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
EOC
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require 'lims-core/persistence/persistor'
|
|
2
|
+
require 'modularity'
|
|
3
|
+
|
|
4
|
+
module Lims::Core
|
|
5
|
+
module Persistence
|
|
6
|
+
module PersistableTrait
|
|
7
|
+
as_trait do |args={}|
|
|
8
|
+
# Define basic persistor
|
|
9
|
+
model_name = self.name.split('::').last
|
|
10
|
+
persistor_name = "#{model_name}Persistor"
|
|
11
|
+
class_eval <<-EOC
|
|
12
|
+
# define Persistor class
|
|
13
|
+
class #{persistor_name} < Persistor
|
|
14
|
+
does 'lims/core/persistence/persistor', #{name}, #{args.inspect}
|
|
15
|
+
end
|
|
16
|
+
EOC
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
module PersistorTrait
|
|
20
|
+
as_trait do |model, args|
|
|
21
|
+
self::Model=model
|
|
22
|
+
model_name = model.name.split('::').last
|
|
23
|
+
parents = []
|
|
24
|
+
deletable_parents = []
|
|
25
|
+
session_names = {}
|
|
26
|
+
skip_parents_for_attributes = {}
|
|
27
|
+
|
|
28
|
+
args[:parents].andtap do |_parents|
|
|
29
|
+
# preprocess parents to get a list
|
|
30
|
+
|
|
31
|
+
_parents.each do |parent|
|
|
32
|
+
if parent.is_a? Hash
|
|
33
|
+
name = parent[:name].to_s
|
|
34
|
+
session_names[name] = parent[:session_name] || name
|
|
35
|
+
skip_parents_for_attributes[name] = parent[:skip_parents_for_attributes]
|
|
36
|
+
deletable_parents << name if parent[:deletable]
|
|
37
|
+
else
|
|
38
|
+
name = parent.to_s
|
|
39
|
+
session_names[name] = name
|
|
40
|
+
end
|
|
41
|
+
parents << name
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
children = []
|
|
46
|
+
deletable_children = []
|
|
47
|
+
args[:children].andtap do |_children|
|
|
48
|
+
_children.each do |child|
|
|
49
|
+
if child.is_a? Hash
|
|
50
|
+
name = child[:name].to_s
|
|
51
|
+
deletable_children << name if child[:deletable]
|
|
52
|
+
else
|
|
53
|
+
name = child.to_s
|
|
54
|
+
session_names[name] = name
|
|
55
|
+
end
|
|
56
|
+
children << name
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if parents.size >= 1
|
|
61
|
+
class_eval <<-EOC
|
|
62
|
+
def filter_attributes_on_load(attributes)
|
|
63
|
+
attributes.mash do |k, v|
|
|
64
|
+
case k
|
|
65
|
+
#{ parents.map do |p|
|
|
66
|
+
"when :#{p}_id then [:#{p}, (@session_#{p} ||= @session.#{session_names[p]})[v]]"
|
|
67
|
+
end.join(';')
|
|
68
|
+
}
|
|
69
|
+
else [k,v]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def attribute_for(key)
|
|
75
|
+
{
|
|
76
|
+
#{ parents.map {|p| "#{p}: '#{p}_id'" }.join(',')}
|
|
77
|
+
}[key]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parents(resource)
|
|
81
|
+
[
|
|
82
|
+
#{ parents.map do |p|
|
|
83
|
+
"resource.#{p}"
|
|
84
|
+
end.join(',') }
|
|
85
|
+
].compact
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parents_for_attributes(attributes)
|
|
89
|
+
[
|
|
90
|
+
#{ parents.reject{|p| skip_parents_for_attributes[p] }.map do |p|
|
|
91
|
+
"(@session_#{p} ||= @session.#{session_names[p]}).state_for_id(attributes[:#{p}_id])"
|
|
92
|
+
end.join(',') }
|
|
93
|
+
]
|
|
94
|
+
end
|
|
95
|
+
EOC
|
|
96
|
+
end
|
|
97
|
+
unless children.empty?
|
|
98
|
+
class_eval <<-EOC
|
|
99
|
+
def children(resource)
|
|
100
|
+
[].tap do |list|
|
|
101
|
+
#{
|
|
102
|
+
children.map do |child|
|
|
103
|
+
"children_#{child}(resource, list)"
|
|
104
|
+
end.join(';')
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def load_children(states)
|
|
110
|
+
#{
|
|
111
|
+
children.map do |child|
|
|
112
|
+
"#{child}.find_by(:#{model_name.snakecase}_id => states.map(&:id))"
|
|
113
|
+
end.join(';')
|
|
114
|
+
}
|
|
115
|
+
1
|
|
116
|
+
end
|
|
117
|
+
EOC
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
unless deletable_children.empty?
|
|
121
|
+
class_eval <<-EOC
|
|
122
|
+
def deletable_children(resource)
|
|
123
|
+
[].tap do |list|
|
|
124
|
+
#{
|
|
125
|
+
deletable_children.map do |child|
|
|
126
|
+
"children_#{child}(resource, list)"
|
|
127
|
+
end.join(';')
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
EOC
|
|
132
|
+
end
|
|
133
|
+
unless deletable_parents.empty?
|
|
134
|
+
class_eval <<-EOC
|
|
135
|
+
def deletable_parents(resource)
|
|
136
|
+
[].tap do |list|
|
|
137
|
+
#{
|
|
138
|
+
deletable_parents.map do |parent|
|
|
139
|
+
"resource.#{parent}.andtap { |p| list << p }"
|
|
140
|
+
end.join(';')
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
EOC
|
|
145
|
+
end
|
|
146
|
+
self
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# vi: ts=2:sts=2:et:sw=2 spell:spelllang=en
|
|
2
|
+
|
|
3
|
+
require 'lims-core/persistence/identity_map'
|
|
4
|
+
|
|
5
|
+
module Lims::Core
|
|
6
|
+
module Persistence
|
|
7
|
+
# @abstract Base class for all the persistors, needs to implements a `self.model`
|
|
8
|
+
# returning the class to persist.
|
|
9
|
+
# A persistor , is used to save and load it's cousin class.
|
|
10
|
+
# The specific code of a persistor should be extended by writting
|
|
11
|
+
# a persistor class within the class to persist and module corresponding to the store.
|
|
12
|
+
# The common Persistor architecture would be like this (let's consider we have a Plate class and a Sequel Persistor).
|
|
13
|
+
# @code
|
|
14
|
+
# module SequelPersistor
|
|
15
|
+
# end
|
|
16
|
+
# Class Plate
|
|
17
|
+
# Common to all store
|
|
18
|
+
# class PlatePersistor < Persistence::Persistor
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# class PlateSequelPersistor < PlatePersistor
|
|
22
|
+
# include SequelPersistor
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
# if a base persistor exists for a class but not the store specific one (PlatePersistor exists
|
|
26
|
+
# but PlateSequelPersistor not). If there is a store pecific Persistor module (like SequelPersistor).
|
|
27
|
+
# The equivalent of PlateSequelPersistor will be generated on the fly by deriving the base one and including the mixin.
|
|
28
|
+
# Persistor needs to be registered to be accessible form the session.
|
|
29
|
+
# However, if NO_AUTO_REGISTRATION is not enabled persistors will register themselves. In that case,
|
|
30
|
+
# they will need to be defined in class to persist see {register_model}.
|
|
31
|
+
# If a base peristor for exists for a class but there is no
|
|
32
|
+
# Each instance can get an identity map, and or parameter
|
|
33
|
+
# specific to a session/thread.
|
|
34
|
+
# * Methods relative to store are
|
|
35
|
+
# - insert : a new object to the store
|
|
36
|
+
# - delete : remove an object fromt the store
|
|
37
|
+
# - update : modify an existing object from the store.
|
|
38
|
+
# - retrieve : get an object from the store.
|
|
39
|
+
# - bulk_<method> vs <method> refers to method acting on a list of states
|
|
40
|
+
# instead of an individual object. Althoug only one version needs to be implemted
|
|
41
|
+
# , the bulk version is prefered for performance reason.
|
|
42
|
+
# - raw_<method_ refers when exists to the physical action done to the store
|
|
43
|
+
# without any side effect on the Session or Persistor. They should not normally be called.
|
|
44
|
+
# * Methods relative to parents/children
|
|
45
|
+
# - parents : resources needed to be saved BEFORE the resource itself.
|
|
46
|
+
# - children : resources needed to be save AFTER the resource itself.
|
|
47
|
+
# - deletable_children : resources which needs to be deleted BEFORE the resource itself.
|
|
48
|
+
# - deletable_parent : resources which needs to be deleted AFTER the resource itself.
|
|
49
|
+
class Persistor
|
|
50
|
+
|
|
51
|
+
# Raised if there is any duplicate in the identity maps
|
|
52
|
+
class DuplicateError < RuntimeError
|
|
53
|
+
def initialize(persistor, value)
|
|
54
|
+
super("#{value.inspect} already exists for persistor #{persistor.model}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#Raised if the `id` is already associated to a different `object`
|
|
59
|
+
class DuplicateIdError <DuplicateError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#Raised if the `object` is already associated to a different `id`
|
|
63
|
+
class DuplicateObjectError < DuplicateError
|
|
64
|
+
end
|
|
65
|
+
# Performs an autoregistration if needed.
|
|
66
|
+
# Autoregistration can be skipped by defined NO_AUTO_REGISTRATION
|
|
67
|
+
# on the model class.
|
|
68
|
+
# See {Persistor::register_model}.
|
|
69
|
+
def self.inherited(subclass)
|
|
70
|
+
register_model(subclass)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Register a sub-persistor to the {Session}.
|
|
74
|
+
# The name used to register the persistor would be
|
|
75
|
+
# either the name of the model (parent) class
|
|
76
|
+
# or if SESSION_NAME is specified on the model : SESSION_NAME
|
|
77
|
+
# @param [Class] subclass
|
|
78
|
+
def self.register_model(subclass)
|
|
79
|
+
model = subclass.parent_scope
|
|
80
|
+
return if model::const_defined? :NO_AUTO_REGISTRATION
|
|
81
|
+
|
|
82
|
+
name =\
|
|
83
|
+
if model::const_defined? :SESSION_NAME
|
|
84
|
+
model::SESSION_NAME
|
|
85
|
+
else
|
|
86
|
+
name = model.name.split('::').pop
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Session::register_model(name, model)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def initialize (session, *args, &block)
|
|
93
|
+
@session = session
|
|
94
|
+
@id_to_state = Hash.new { |h,k| h[k] = ResourceState.new(nil, self, k) }
|
|
95
|
+
@object_to_state = Hash.new { |h,k| h[k] = ResourceState.new(k, self) }
|
|
96
|
+
super(*args, &block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Associate class (without persistence).
|
|
100
|
+
# @return [Class]
|
|
101
|
+
def model
|
|
102
|
+
self.class::Model
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Load a model by different criteria. Could be either :
|
|
106
|
+
# - an Id
|
|
107
|
+
# - a Hash
|
|
108
|
+
# - a list of Ids
|
|
109
|
+
# This method will return either a single object or a list of object,
|
|
110
|
+
# depending of the parameter.
|
|
111
|
+
# Note that loaded object are automatically _added_ to the session.
|
|
112
|
+
# @param [Fixnum, Hash] id the id in the database
|
|
113
|
+
# @param [Boolean] single or list of object to return
|
|
114
|
+
# @return [Object,nil] nil if object not found.
|
|
115
|
+
def [](id, single=true)
|
|
116
|
+
case id
|
|
117
|
+
when Fixnum then retrieve(id)
|
|
118
|
+
when Hash then find_by(filter_attributes_on_save(id), single)
|
|
119
|
+
when Array, Enumerable then bulk_retrieve(id)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get the id from an object from the cache.
|
|
124
|
+
# @param [Resource] object object to find the id for.
|
|
125
|
+
# @return [Id, Nil]
|
|
126
|
+
def id_for(object)
|
|
127
|
+
state_for(object).andtap { |state| state.id }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Get the object from a given id.
|
|
131
|
+
# @param [Fixnum] id
|
|
132
|
+
# @return [Resourec, Nil]
|
|
133
|
+
def object_for(id)
|
|
134
|
+
@id_to_state[id].andtap(&:resource)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Returns the state proxy of an object.
|
|
139
|
+
# Creates it if needed.
|
|
140
|
+
# @param [Resource] object
|
|
141
|
+
# @return [ResourceState]
|
|
142
|
+
def state_for(object)
|
|
143
|
+
@object_to_state[object]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def state_for?(object)
|
|
148
|
+
@object_to_state.include?(object)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Returns the state proxy of an object fromt its id (in cache).
|
|
152
|
+
# Creates the state if needed.
|
|
153
|
+
# @param [Id] object
|
|
154
|
+
# @return [ResourceState]
|
|
155
|
+
def state_for_id(id)
|
|
156
|
+
@id_to_state[id]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Updates the cache so id_to_state
|
|
160
|
+
# reflects state.id
|
|
161
|
+
# @param [ResourceState]
|
|
162
|
+
def bind_state_to_id(state)
|
|
163
|
+
raise RuntimeError, 'Invalid state' if state.persistor != self
|
|
164
|
+
raise DuplicateIdError.new(self, state.id)if @id_to_state.include?(state.id)
|
|
165
|
+
on_object_load(state)
|
|
166
|
+
@id_to_state[state.id] = state
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Called by Persistor to inform the session
|
|
170
|
+
# about the loading of an object.
|
|
171
|
+
# MUST be called by persistors creating Resources.
|
|
172
|
+
# @param [ResourceState]
|
|
173
|
+
def on_object_load(state)
|
|
174
|
+
@session.manage_state(state)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Update the cache
|
|
178
|
+
def bind_state_to_resource(state)
|
|
179
|
+
raise RuntimeError, 'Invalobject state' if state.persistor != self
|
|
180
|
+
raise DuplicateIdError.new(self, state.resource) if @object_to_state.include?(state.resource)
|
|
181
|
+
@object_to_state[state.resource] = state
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Creates a new object from a Hash and associate it to its id
|
|
185
|
+
# @param [Id] id id of the new object
|
|
186
|
+
# @param [Hash] attributes of the new object.
|
|
187
|
+
# @return [Resource]
|
|
188
|
+
def new_object(id, attributes)
|
|
189
|
+
id = attributes.delete(primary_key)
|
|
190
|
+
model.new(filter_attributes_on_load(attributes)).tap do |resource|
|
|
191
|
+
state = state_for_id(id)
|
|
192
|
+
state.resource = resource
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Computes "dirty_key" of an object.
|
|
197
|
+
# The dirty key is used to decide if an object
|
|
198
|
+
# has been modified or not.
|
|
199
|
+
# @param [ Resource]
|
|
200
|
+
# @return [Object]
|
|
201
|
+
def dirty_key_for(resource)
|
|
202
|
+
if resource && @session.dirty_attribute_strategy
|
|
203
|
+
@session.dirty_key_for(filter_attributes_on_save(resource.attributes_for_dirty))
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Delete all invalid object loaded by a persistor.
|
|
208
|
+
# Typically invalid object are association which doesn't exist anymore
|
|
209
|
+
def purge_invalid_object
|
|
210
|
+
to_delete = StateGroup.new(self, [])
|
|
211
|
+
@object_to_state.each do |object, state|
|
|
212
|
+
to_delete << state if invalid_resource?(object)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
to_delete.destroy
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Create or get one or object matching the criteria
|
|
219
|
+
# @param [Hash] criteria, map of (attributes, value) to match
|
|
220
|
+
# @param [Boolean] single wether to check for uniquess or not
|
|
221
|
+
# @return [Object,nil,Array<Object>] an Object or and Array depending of single.
|
|
222
|
+
#
|
|
223
|
+
def find_by(criteria, single=false)
|
|
224
|
+
ids = ids_for(criteria)
|
|
225
|
+
|
|
226
|
+
if single
|
|
227
|
+
raise RuntimeError, "More than one object match the criteria" if ids.size > 1
|
|
228
|
+
return nil if ids.size < 1
|
|
229
|
+
self[ids].first
|
|
230
|
+
else
|
|
231
|
+
self[ids]
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
protected :find_by
|
|
235
|
+
|
|
236
|
+
# compute a list of ids matching the criteria
|
|
237
|
+
# @param [Hash] criteria list of attribute/value pais
|
|
238
|
+
# @return [Array<Id>]
|
|
239
|
+
def ids_for(criteria)
|
|
240
|
+
raise NotImplementedError
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# @abstract
|
|
244
|
+
# Returns the number of object in the store
|
|
245
|
+
# @return [Fixnum]
|
|
246
|
+
def count
|
|
247
|
+
raise NotImplementedError
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @abstract
|
|
251
|
+
# Load a slice. Doesn't return an object but a hash
|
|
252
|
+
# allowing to build it.
|
|
253
|
+
# @param [Fixnum] start (0 based)
|
|
254
|
+
# @param [Fixnum] length
|
|
255
|
+
# @yieldparam [Fixnum] key
|
|
256
|
+
# @yieldparam [Hash] attributes of the object
|
|
257
|
+
def for_each_in_slice(start, length)
|
|
258
|
+
raise NotImplementedError
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get a slice of object by offset, length.
|
|
262
|
+
# +start+ here is an offset (starting at 0) not an Id.
|
|
263
|
+
# @param [Fixnum] start (0 based)
|
|
264
|
+
# @param [Fixnum] length
|
|
265
|
+
# @return [Enumerable<Hash>]
|
|
266
|
+
def slice(start, length)
|
|
267
|
+
to_load = StateGroup.new(self, [])
|
|
268
|
+
for_each_in_slice(start, length) do |att|
|
|
269
|
+
to_load << new_from_attributes(att)
|
|
270
|
+
end
|
|
271
|
+
to_load.load.map(&:resource)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Inserts objects in the underlying store AND manages them.
|
|
275
|
+
# This method only care about the objects themselves not about
|
|
276
|
+
# theirs parents or children.
|
|
277
|
+
# The physical insert in the store must be specified for each store.
|
|
278
|
+
def bulk_insert(states, *params)
|
|
279
|
+
states.map { |state| insert(state, *params) }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Remove object form the underlying store and Manages them.
|
|
283
|
+
# This method only care about the objects themselves not about
|
|
284
|
+
# theirs parents or children.
|
|
285
|
+
def bulk_delete(states, *params)
|
|
286
|
+
# delete theme but leave them in cache
|
|
287
|
+
# in case they need to be displayed.
|
|
288
|
+
states.each do |state|
|
|
289
|
+
state.id.andtap { |id| @id_to_state.delete(id) }
|
|
290
|
+
state.resource #.andtap { |object| @object_to_state.delete(object) }
|
|
291
|
+
end
|
|
292
|
+
bulk_delete_raw(states.map(&:id).compact, *params)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# @abstract
|
|
296
|
+
# Physically remove objects from a store.
|
|
297
|
+
def bulk_delete_raw(states, *params)
|
|
298
|
+
raise NotImplementedError
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
%w(insert update delete_raw).each do |method|
|
|
302
|
+
class_eval %Q{
|
|
303
|
+
#bulk_#{method} and #{method} can be both implemented from each other.
|
|
304
|
+
#raise a NotImplementedError is none of them have been implemented
|
|
305
|
+
def #{method}(param, *params)
|
|
306
|
+
raise NotImplementedError if @__simple_#{method}
|
|
307
|
+
@__simple_#{method} = true
|
|
308
|
+
bulk_#{method}([param], *params).andtap do |results|
|
|
309
|
+
@__simple_#{method} = false
|
|
310
|
+
results.first
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
}
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Retrieves an object from it's id.
|
|
317
|
+
# Doesn't load it if it's been alreday loaded.
|
|
318
|
+
# @param [Id] id
|
|
319
|
+
# @return [Object, nil]
|
|
320
|
+
def retrieve(id, *params)
|
|
321
|
+
object_for(id).andtap { |o| return o }
|
|
322
|
+
objects = bulk_retrieve([id], *params)
|
|
323
|
+
return objects.first if objects && objects.size == 1
|
|
324
|
+
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Retreives a list of objects .
|
|
328
|
+
# @param[Array<Id>] ids
|
|
329
|
+
# @return [Array<Object]
|
|
330
|
+
def bulk_retrieve(ids, *params)
|
|
331
|
+
# create a list of states and load them
|
|
332
|
+
states = StateGroup.new(self, ids.map do |id|
|
|
333
|
+
@id_to_state[id]
|
|
334
|
+
end)
|
|
335
|
+
|
|
336
|
+
states.load
|
|
337
|
+
return StateList.new(states.map { |state| state.resource })
|
|
338
|
+
|
|
339
|
+
# we need to separate object which need to be loaded
|
|
340
|
+
# from the one which are already in cache
|
|
341
|
+
to_load = ids.reject { |id| id == nil || @id_to_state.include?(id) }
|
|
342
|
+
loaded_states = bulk_load_raw_attributes(to_load, *params) do |att|
|
|
343
|
+
id = att.delete(primary_key)
|
|
344
|
+
new_state_for_attribute(id, att).resource
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
bulk_retrieve_children(new_states, *params)
|
|
348
|
+
#bulk_retrieve_parent(new_states, *params)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
ids.map { |id| object_for(id) }
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Updates the store and manages object.
|
|
355
|
+
# Doesn't care of children or parents.
|
|
356
|
+
# @param [Array<ResourceState] states
|
|
357
|
+
def bulk_update(states, *params)
|
|
358
|
+
attributes = states.map do |state|
|
|
359
|
+
filter_attributes_on_save(state.resource.attributes).merge(primary_key => state.id)
|
|
360
|
+
end
|
|
361
|
+
bulk_update_raw_attributes(attributes, *params)
|
|
362
|
+
states.each do |state|
|
|
363
|
+
state.updated
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
%w(parents children deletable_children deletable_parents).each do |m|
|
|
368
|
+
# @method #{m}_for
|
|
369
|
+
# @param [Resource]
|
|
370
|
+
# @return [Array<ResourceState>]
|
|
371
|
+
define_method "#{m}_for" do |resource|
|
|
372
|
+
@session.states_for(public_send(m, resource))
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# List of parents of object, i.e. object which need to be saved BEFORE it.
|
|
377
|
+
# Default implementation get all Resource attributes.
|
|
378
|
+
# @param [Resource] resource
|
|
379
|
+
# @return [Array<Resource>]
|
|
380
|
+
def parents(resource)
|
|
381
|
+
resource.attributes.values.select { |v| v.is_a? Resource }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# List of children , i.e, object which need to be saved AFTER it.
|
|
385
|
+
# @param [Resource] resource
|
|
386
|
+
# @return [Array<Resource>]
|
|
387
|
+
def children(resource)
|
|
388
|
+
[]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# @todo
|
|
392
|
+
def deletable_children(resource)
|
|
393
|
+
[]
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def deletable_parents(resource)
|
|
397
|
+
[]
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# if a resource is invalid and need to be deleted.
|
|
401
|
+
# For example an association proxy corresponding
|
|
402
|
+
# to an old relation.
|
|
403
|
+
def invalid_resource?(resource)
|
|
404
|
+
resource.respond_to?(:invalid?) && resource.invalid?
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
protected
|
|
410
|
+
# The primary key
|
|
411
|
+
# @return [Symbol]
|
|
412
|
+
def primary_key()
|
|
413
|
+
:id
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Transform store fields to object attributes
|
|
417
|
+
# This can be used to change the name of an attribute (its key)
|
|
418
|
+
# or its value or both (example resource to resource_id)
|
|
419
|
+
# This is the reverse of {#filter_attributes_on_save}
|
|
420
|
+
# @param [Hash] attributes
|
|
421
|
+
# @return [Hash]
|
|
422
|
+
def filter_attributes_on_load(attributes)
|
|
423
|
+
if block_given?
|
|
424
|
+
attributes.mash do |k,v|
|
|
425
|
+
yield(k,v) || [k,v]
|
|
426
|
+
end
|
|
427
|
+
else attributes
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def parents_for_attributes(attributes)
|
|
432
|
+
[]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
public :parents_for_attributes
|
|
436
|
+
def load_children(states, *params)
|
|
437
|
+
[]
|
|
438
|
+
end
|
|
439
|
+
public :load_children
|
|
440
|
+
|
|
441
|
+
def new_from_attributes(attributes)
|
|
442
|
+
id = attributes.delete(primary_key)
|
|
443
|
+
resource = block_given? ? yield(attributes) : model.new(filter_attributes_on_load(attributes))
|
|
444
|
+
state_for_id(id).tap { |state| state.resource = resource }
|
|
445
|
+
end
|
|
446
|
+
public :new_from_attributes
|
|
447
|
+
|
|
448
|
+
# Transform object attributes to store fields
|
|
449
|
+
# This can be used to change the name of an attribute (its key)
|
|
450
|
+
# or its value or both (example resource to resource_id)
|
|
451
|
+
# @param [Hash] attributes
|
|
452
|
+
# @return [Hash]
|
|
453
|
+
def filter_attributes_on_save(attributes)
|
|
454
|
+
attributes.mash do |k, v|
|
|
455
|
+
if block_given?
|
|
456
|
+
result = yield(k,v)
|
|
457
|
+
next result if result
|
|
458
|
+
end
|
|
459
|
+
key = attribute_for(k)
|
|
460
|
+
if key && key != k
|
|
461
|
+
[key, @session.id_for(v) ]
|
|
462
|
+
else
|
|
463
|
+
[k, v]
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def attribute_for(key)
|
|
470
|
+
key
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def self.association_class(association, &block)
|
|
474
|
+
snake = association.snakecase
|
|
475
|
+
association_class = class_eval <<-EOC
|
|
476
|
+
class #{association}
|
|
477
|
+
include Lims::Core::Resource
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def #{snake}
|
|
481
|
+
@session.#{snake}_persistor
|
|
482
|
+
end
|
|
483
|
+
#{association}
|
|
484
|
+
EOC
|
|
485
|
+
association_class.class_eval(&block)
|
|
486
|
+
association_class.class_eval do
|
|
487
|
+
does "lims/core/persistence/persist_association", self
|
|
488
|
+
end
|
|
489
|
+
association_class
|
|
490
|
+
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
require 'lims-core/persistence/session'
|