ooor 2.0.3 → 2.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +1 -0
- data/bin/ooor +1 -0
- data/lib/ooor.rb +2 -0
- data/lib/ooor/associations.rb +30 -40
- data/lib/ooor/base.rb +22 -113
- data/lib/ooor/callbacks.rb +18 -0
- data/lib/ooor/field_methods.rb +133 -85
- data/lib/ooor/helpers/core_helpers.rb +9 -63
- data/lib/ooor/mini_active_resource.rb +0 -18
- data/lib/ooor/naming.rb +2 -2
- data/lib/ooor/persistence.rb +140 -0
- data/lib/ooor/railtie.rb +2 -1
- data/lib/ooor/reflection.rb +11 -399
- data/lib/ooor/reflection_ooor.rb +36 -11
- data/lib/ooor/relation.rb +48 -23
- data/lib/ooor/type_casting.rb +108 -75
- data/lib/ooor/version.rb +1 -1
- data/spec/ooor_spec.rb +133 -27
- metadata +35 -40
@@ -19,9 +19,9 @@ Ooor.xtend('ir.module.module') do
|
|
19
19
|
dependency_modules = []
|
20
20
|
modules.select { |m| m.dependencies_id }.each do |mod|
|
21
21
|
mod.dependencies_id.each do |dep|
|
22
|
-
dep_module =
|
23
|
-
|
24
|
-
|
22
|
+
dep_module = self.find(:first,
|
23
|
+
:domain => [['name', '=', dep.name]],
|
24
|
+
:fields => ['id', 'state', 'dependencies_id'])
|
25
25
|
if dep_module.nil?
|
26
26
|
raise RuntimeError, "#{dep.name} not found"
|
27
27
|
end
|
@@ -32,66 +32,13 @@ Ooor.xtend('ir.module.module') do
|
|
32
32
|
dependency_modules.uniq { |m| m.id }
|
33
33
|
end
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# - modules : A [] of valid IrModuleModule instance
|
40
|
-
# Return
|
41
|
-
# - True
|
42
|
-
# Usage Example:
|
43
|
-
# res = IrModuleModule.install_modules(@openerp, modules)
|
44
|
-
def self.install_modules(modules, dependencies=false)
|
45
|
-
res = true
|
46
|
-
if dependencies
|
47
|
-
dependency_modules = get_dependencies(modules)
|
48
|
-
modules.concat(dependency_modules) if dependency_modules
|
49
|
-
end
|
50
|
-
modules_toinstall_ids = []
|
51
|
-
modules_toupgrade_ids = []
|
52
|
-
# If not installed, do it. Otherwise update it
|
53
|
-
modules.each do |m|
|
54
|
-
if m.state == 'uninstalled'
|
55
|
-
m.state = 'to install'
|
56
|
-
m.save
|
57
|
-
modules_toinstall_ids << m.id
|
58
|
-
elsif m.state == 'installed'
|
59
|
-
m.state = 'to upgrade'
|
60
|
-
m.save
|
61
|
-
modules_toupgrade_ids << m.id
|
62
|
-
elsif m.state == 'to install'
|
63
|
-
modules_toinstall_ids << m.id
|
64
|
-
elsif m.state == 'to upgrade'
|
65
|
-
modules_toupgrade_ids << m.id
|
66
|
-
end
|
67
|
-
end
|
68
|
-
#First installed required modules, then upgrade the others
|
69
|
-
upgrade = BaseModuleUpgrade.create()
|
70
|
-
upgrade.upgrade_module()
|
71
|
-
# IrModuleModule.button_install(modules_toinstall_ids)
|
72
|
-
# IrModuleModule.button_upgrade(modules_toupgrade_ids)
|
73
|
-
|
74
|
-
if res
|
75
|
-
return true
|
76
|
-
else
|
77
|
-
raise "!!! --- HELPER ERROR : install_modules was unable to install needed modules.."
|
78
|
-
end
|
79
|
-
openerp.load_models() # reload in order to have model Classes for modules installed
|
80
|
-
end
|
81
|
-
|
82
|
-
def print_uml
|
83
|
-
l = IrModelData.find(:all, :domain => {:model=>"ir.model", :module=>name})
|
84
|
-
model_names = []
|
85
|
-
l.each {|i| model_names << i.name.gsub('_', '.').gsub(/^model.report/, '').gsub(/^model./, '')}
|
86
|
-
classes = []
|
87
|
-
model_names.each {|i| begin classes << Object.const_get(IrModel.class_name_from_model_key i); rescue; end}
|
88
|
-
classes.reject! {|m| m.openerp_model.index("report")} #NOTE we would need a more robust test
|
89
|
-
begin
|
90
|
-
classes.reject! {|m| IrModel.read(m.openerp_id, ['osv_memory'])['osv_memory']}
|
91
|
-
rescue
|
35
|
+
def self.install_modules(modules)
|
36
|
+
modules = modules.map { |name| self.find(:first, domain: {name: name})}
|
37
|
+
modules.each do |mod|
|
38
|
+
mod.button_install unless mod.state == "installed"
|
92
39
|
end
|
93
|
-
|
94
|
-
|
40
|
+
wizard = BaseModuleUpgrade.create
|
41
|
+
wizard.upgrade_module
|
95
42
|
end
|
96
43
|
|
97
44
|
def print_dependency_graph
|
@@ -122,7 +69,6 @@ Ooor.xtend('ir.module.module') do
|
|
122
69
|
system("tred < #{self.name}-pre.dot > #{self.name}.dot")
|
123
70
|
cmd_line2 = "dot -Tcmapx -o#{self.name}.map -Tpng -o#{self.name}.png #{self.name}.dot"
|
124
71
|
system(cmd_line2)
|
125
|
-
|
126
72
|
end
|
127
73
|
|
128
74
|
end
|
@@ -68,24 +68,6 @@ module Ooor
|
|
68
68
|
self.class.__send__(:split_options, options)
|
69
69
|
end
|
70
70
|
|
71
|
-
def method_missing(method_symbol, *arguments) #:nodoc:
|
72
|
-
method_name = method_symbol.to_s
|
73
|
-
|
74
|
-
if method_name =~ /(=|\?)$/
|
75
|
-
case $1
|
76
|
-
when "="
|
77
|
-
attributes[$`] = arguments.first
|
78
|
-
when "?"
|
79
|
-
attributes[$`]
|
80
|
-
end
|
81
|
-
else
|
82
|
-
return attributes[method_name] if attributes.include?(method_name)
|
83
|
-
# not set right now but we know about it
|
84
|
-
return nil if known_attributes.include?(method_name)
|
85
|
-
super
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
71
|
include ActiveModel::Conversion
|
90
72
|
include ActiveModel::Serializers::JSON
|
91
73
|
include ActiveModel::Serializers::Xml
|
data/lib/ooor/naming.rb
CHANGED
@@ -10,7 +10,7 @@ module Ooor
|
|
10
10
|
namespace = self.parents.detect do |n|
|
11
11
|
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
12
12
|
end
|
13
|
-
ActiveModel::Name.new(self, namespace, self.description).tap do |r|
|
13
|
+
ActiveModel::Name.new(self, namespace, self.description || self.openerp_model).tap do |r|
|
14
14
|
def r.param_key
|
15
15
|
@klass.openerp_model.gsub('.', '_')
|
16
16
|
end
|
@@ -52,7 +52,7 @@ module Ooor
|
|
52
52
|
def alias(context={})
|
53
53
|
# NOTE in v8, see if we can use ModelConvert here https://github.com/akretion/openerp-addons/blob/trunk-website-al/website/models/ir_http.py#L126
|
54
54
|
if connection.config[:aliases]
|
55
|
-
lang = context['lang'] || connection.config[
|
55
|
+
lang = context['lang'] || connection.config['lang'] || 'en_US'
|
56
56
|
if alias_data = connection.config[:aliases][lang]
|
57
57
|
alias_data.select{|key, value| value == openerp_model }.keys[0] || openerp_model
|
58
58
|
else
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# OOOR: OpenObject On Ruby
|
2
|
+
# Copyright (C) 2009-2014 Akretion LTDA (<http://www.akretion.com>).
|
3
|
+
# Author: Raphaël Valyi
|
4
|
+
# Licensed under the MIT license, see MIT-LICENSE file
|
5
|
+
|
6
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
7
|
+
require 'active_model/attribute_methods'
|
8
|
+
require 'active_model/dirty'
|
9
|
+
require 'ooor/errors'
|
10
|
+
|
11
|
+
module Ooor
|
12
|
+
|
13
|
+
# the base class for proxies to OpenERP objects
|
14
|
+
module Persistence
|
15
|
+
|
16
|
+
def load(attributes, remove_root=false, persisted=false)#an attribute might actually be a association too, will be determined here
|
17
|
+
self.class.reload_fields_definition(false, object_session)
|
18
|
+
raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
|
19
|
+
@prefix_options, attributes = split_options(attributes)
|
20
|
+
@associations ||= {}
|
21
|
+
@attributes ||= {}
|
22
|
+
@loaded_associations = {}
|
23
|
+
attributes.each do |key, value|
|
24
|
+
self.send "#{key}=".to_sym, value if self.respond_to?("#{key}=".to_sym)
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
#takes care of reading OpenERP default field values.
|
30
|
+
def initialize(attributes = {}, default_get_list=false, context={}, persisted=false)
|
31
|
+
@attributes = {}
|
32
|
+
@prefix_options = {}
|
33
|
+
@ir_model_data_id = attributes.delete(:ir_model_data_id)
|
34
|
+
@object_session = {}
|
35
|
+
@object_session = HashWithIndifferentAccess.new(context)
|
36
|
+
@persisted = persisted
|
37
|
+
self.class.reload_fields_definition(false, @object_session)
|
38
|
+
if default_get_list == []
|
39
|
+
load(attributes)
|
40
|
+
else
|
41
|
+
load_with_defaults(attributes, default_get_list)
|
42
|
+
end.tap do
|
43
|
+
if id
|
44
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new # see ActiveModel::Dirty reset_changes
|
45
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Saves (+create+) or \updates (+write+) a resource. Delegates to +create+ if the object is \new,
|
51
|
+
# +update+ if it exists.
|
52
|
+
def save(context={}, reload=true)
|
53
|
+
create_or_update(context, reload)
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_or_update(context={}, reload=true)
|
57
|
+
run_callbacks :save do
|
58
|
+
new? ? create_record(context, reload) : update_record(context, reload)
|
59
|
+
end
|
60
|
+
rescue ValidationError => e
|
61
|
+
e.extract_validation_error!(errors)
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create (i.e., \save to OpenERP service) the \new resource.
|
66
|
+
def create(context={}, reload=true)
|
67
|
+
create_or_update(context, reload)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_record(context={}, reload=true)
|
71
|
+
run_callbacks :create do
|
72
|
+
self.id = rpc_execute('create', to_openerp_hash, context)
|
73
|
+
if @ir_model_data_id
|
74
|
+
IrModelData.create(model: self.class.openerp_model,
|
75
|
+
'module' => @ir_model_data_id[0],
|
76
|
+
'name' => @ir_model_data_id[1],
|
77
|
+
'res_id' => self.id)
|
78
|
+
end
|
79
|
+
@persisted = true
|
80
|
+
reload_fields(context) if reload
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_attributes(attributes, context={}, reload=true)
|
85
|
+
load(attributes, false) && save(context, reload)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Update the resource on the remote service.
|
89
|
+
def update(context={}, reload=true, keys=nil)
|
90
|
+
create_or_update(context, reload, keys)
|
91
|
+
end
|
92
|
+
|
93
|
+
def update_record(context={}, reload=true)
|
94
|
+
run_callbacks :update do
|
95
|
+
rpc_execute('write', [self.id], to_openerp_hash, context)
|
96
|
+
reload_fields(context) if reload
|
97
|
+
@persisted = true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
#Deletes the record in OpenERP and freezes this instance to reflect that no changes should be made (since they can’t be persisted).
|
102
|
+
def destroy(context={})
|
103
|
+
run_callbacks :destroy do
|
104
|
+
rpc_execute('unlink', [self.id], context)
|
105
|
+
@destroyed = true
|
106
|
+
freeze
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
#OpenERP copy method, load persisted copied Object
|
111
|
+
def copy(defaults={}, context={})
|
112
|
+
self.class.find(rpc_execute('copy', self.id, defaults, context), context: context)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def load_with_defaults(attributes, default_get_list)
|
118
|
+
defaults = rpc_execute("default_get", default_get_list || self.class.fields.keys + self.class.associations_keys, object_session.dup)
|
119
|
+
attributes = HashWithIndifferentAccess.new(defaults.merge(attributes.reject {|k, v| v.blank? }))
|
120
|
+
load(attributes)
|
121
|
+
end
|
122
|
+
|
123
|
+
def load_on_change_result(result, field_name, field_value)
|
124
|
+
if result["warning"]
|
125
|
+
self.class.logger.info result["warning"]["title"]
|
126
|
+
self.class.logger.info result["warning"]["message"]
|
127
|
+
end
|
128
|
+
attrs = @attributes.merge(field_name => field_value)
|
129
|
+
attrs.merge!(result["value"])
|
130
|
+
load(attrs)
|
131
|
+
end
|
132
|
+
|
133
|
+
def reload_fields(context)
|
134
|
+
record = self.class.find(self.id, context: context)
|
135
|
+
load(record.attributes.merge(record.associations))
|
136
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new # see ActiveModel::Dirty
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
data/lib/ooor/railtie.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "rails/railtie"
|
2
2
|
require "ooor/rack"
|
3
|
+
require "yaml"
|
3
4
|
|
4
5
|
module Ooor
|
5
6
|
class Railtie < Rails::Railtie
|
@@ -29,7 +30,7 @@ module Ooor
|
|
29
30
|
|
30
31
|
def load_config(config_file=nil, env=nil)
|
31
32
|
config_file ||= defined?(Rails.root) && "#{Rails.root}/config/ooor.yml" || 'ooor.yml'
|
32
|
-
@config = HashWithIndifferentAccess.new(YAML.load_file(config_file)[env || 'development'])
|
33
|
+
@config = HashWithIndifferentAccess.new(::YAML.load_file(config_file)[env || 'development'])
|
33
34
|
rescue SystemCallError
|
34
35
|
Ooor.logger.error """failed to load OOOR yaml configuration file.
|
35
36
|
make sure your app has a #{config_file} file correctly set up
|
data/lib/ooor/reflection.rb
CHANGED
@@ -2,7 +2,7 @@ require 'active_support/core_ext/class/attribute'
|
|
2
2
|
require 'active_support/core_ext/object/inclusion'
|
3
3
|
|
4
4
|
# NOTE this is a scoped copy of ActiveRecord reflection.rb
|
5
|
-
# the few necessary hacks are explicited with a FIXME
|
5
|
+
# the few necessary hacks are explicited with a FIXME or a NOTE
|
6
6
|
# an addition Ooor specific reflection module completes this one explicitely
|
7
7
|
module Ooor
|
8
8
|
# = Active Record Reflection
|
@@ -23,18 +23,7 @@ module Ooor
|
|
23
23
|
# MacroReflection class has info for AggregateReflection and AssociationReflection
|
24
24
|
# classes.
|
25
25
|
module ClassMethods
|
26
|
-
def create_reflection(macro, name, options, active_record)
|
27
|
-
case macro
|
28
|
-
when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
|
29
|
-
klass = options[:through] ? ThroughReflection : AssociationReflection
|
30
|
-
reflection = klass.new(macro, name, options, active_record)
|
31
|
-
when :composed_of
|
32
|
-
reflection = AggregateReflection.new(macro, name, options, active_record)
|
33
|
-
end
|
34
|
-
|
35
|
-
self.reflections = self.reflections.merge(name => reflection)
|
36
|
-
reflection
|
37
|
-
end
|
26
|
+
#def create_reflection(macro, name, options, active_record) #NOTE overriden in Ooor
|
38
27
|
|
39
28
|
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
40
29
|
def reflect_on_all_aggregations
|
@@ -64,14 +53,7 @@ module Ooor
|
|
64
53
|
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
|
65
54
|
end
|
66
55
|
|
67
|
-
#
|
68
|
-
#
|
69
|
-
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
|
70
|
-
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
|
71
|
-
#
|
72
|
-
def reflect_on_association(association)
|
73
|
-
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
|
74
|
-
end
|
56
|
+
# def reflect_on_association(association) # NOTE overriden in Ooor
|
75
57
|
|
76
58
|
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
77
59
|
def reflect_on_all_autosave_associations
|
@@ -118,9 +100,9 @@ module Ooor
|
|
118
100
|
#
|
119
101
|
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
|
120
102
|
# <tt>has_many :clients</tt> returns the Client class
|
121
|
-
def klass
|
122
|
-
@klass ||= class_name.constantize
|
123
|
-
end
|
103
|
+
# def klass #NOTE overriden in Ooor
|
104
|
+
# @klass ||= class_name.constantize
|
105
|
+
# end
|
124
106
|
|
125
107
|
# Returns the class name for the macro.
|
126
108
|
#
|
@@ -140,9 +122,9 @@ module Ooor
|
|
140
122
|
active_record == other_aggregation.active_record
|
141
123
|
end
|
142
124
|
|
143
|
-
def sanitized_conditions #:nodoc:
|
144
|
-
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
145
|
-
end
|
125
|
+
# def sanitized_conditions #:nodoc: #NOTE not applicable in Ooor
|
126
|
+
# @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
127
|
+
# end
|
146
128
|
|
147
129
|
private
|
148
130
|
def derive_class_name
|
@@ -158,380 +140,10 @@ module Ooor
|
|
158
140
|
|
159
141
|
# Holds all the meta-data about an association as it was specified in the
|
160
142
|
# Active Record class.
|
161
|
-
class AssociationReflection < MacroReflection #:nodoc:
|
162
|
-
# Returns the target association's class.
|
163
|
-
#
|
164
|
-
# class Author < ActiveRecord::Base
|
165
|
-
# has_many :books
|
166
|
-
# end
|
167
|
-
#
|
168
|
-
# Author.reflect_on_association(:books).klass
|
169
|
-
# # => Book
|
170
|
-
#
|
171
|
-
# <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
|
172
|
-
# a new association object. Use +build_association+ or +create_association+
|
173
|
-
# instead. This allows plugins to hook into association object creation.
|
174
|
-
def klass
|
175
|
-
@klass ||= active_record.send(:compute_type, class_name)
|
176
|
-
end
|
177
|
-
|
178
|
-
def initialize(macro, name, options, active_record)
|
179
|
-
super
|
180
|
-
@collection = macro.in?([:has_many, :has_and_belongs_to_many])
|
181
|
-
end
|
182
|
-
|
183
|
-
# Returns a new, unsaved instance of the associated class. +options+ will
|
184
|
-
# be passed to the class's constructor.
|
185
|
-
def build_association(*options, &block)
|
186
|
-
klass.new(*options, &block)
|
187
|
-
end
|
188
|
-
|
189
|
-
def table_name
|
190
|
-
@table_name ||= klass.table_name
|
191
|
-
end
|
192
|
-
|
193
|
-
def quoted_table_name
|
194
|
-
@quoted_table_name ||= klass.quoted_table_name
|
195
|
-
end
|
196
|
-
|
197
|
-
def foreign_key
|
198
|
-
@foreign_key ||= options[:foreign_key] || derive_foreign_key
|
199
|
-
end
|
200
|
-
|
201
|
-
def foreign_type
|
202
|
-
@foreign_type ||= options[:foreign_type] || "#{name}_type"
|
203
|
-
end
|
204
|
-
|
205
|
-
def type
|
206
|
-
@type ||= options[:as] && "#{options[:as]}_type"
|
207
|
-
end
|
208
|
-
|
209
|
-
def primary_key_column
|
210
|
-
@primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
|
211
|
-
end
|
212
|
-
|
213
|
-
def association_foreign_key
|
214
|
-
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
|
215
|
-
end
|
216
|
-
|
217
|
-
# klass option is necessary to support loading polymorphic associations
|
218
|
-
def association_primary_key(klass = nil)
|
219
|
-
options[:primary_key] || primary_key(klass || self.klass)
|
220
|
-
end
|
221
|
-
|
222
|
-
def active_record_primary_key
|
223
|
-
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
|
224
|
-
end
|
225
|
-
|
226
|
-
def counter_cache_column
|
227
|
-
if options[:counter_cache] == true
|
228
|
-
"#{active_record.name.demodulize.underscore.pluralize}_count"
|
229
|
-
elsif options[:counter_cache]
|
230
|
-
options[:counter_cache].to_s
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
def columns(tbl_name, log_msg)
|
235
|
-
@columns ||= klass.connection.columns(tbl_name, log_msg)
|
236
|
-
end
|
237
|
-
|
238
|
-
def reset_column_information
|
239
|
-
@columns = nil
|
240
|
-
end
|
241
|
-
|
242
|
-
def check_validity!
|
243
|
-
check_validity_of_inverse!
|
244
|
-
end
|
245
|
-
|
246
|
-
def check_validity_of_inverse!
|
247
|
-
unless options[:polymorphic]
|
248
|
-
if has_inverse? && inverse_of.nil?
|
249
|
-
raise InverseOfAssociationNotFoundError.new(self)
|
250
|
-
end
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def through_reflection
|
255
|
-
nil
|
256
|
-
end
|
257
|
-
|
258
|
-
def source_reflection
|
259
|
-
nil
|
260
|
-
end
|
261
|
-
|
262
|
-
# A chain of reflections from this one back to the owner. For more see the explanation in
|
263
|
-
# ThroughReflection.
|
264
|
-
def chain
|
265
|
-
[self]
|
266
|
-
end
|
267
|
-
|
268
|
-
def nested?
|
269
|
-
false
|
270
|
-
end
|
271
|
-
|
272
|
-
# An array of arrays of conditions. Each item in the outside array corresponds to a reflection
|
273
|
-
# in the #chain. The inside arrays are simply conditions (and each condition may itself be
|
274
|
-
# a hash, array, arel predicate, etc...)
|
275
|
-
def conditions
|
276
|
-
[[options[:conditions]].compact]
|
277
|
-
end
|
278
|
-
|
279
|
-
alias :source_macro :macro
|
280
|
-
|
281
|
-
def has_inverse?
|
282
|
-
@options[:inverse_of]
|
283
|
-
end
|
284
|
-
|
285
|
-
def inverse_of
|
286
|
-
if has_inverse?
|
287
|
-
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
|
288
|
-
end
|
289
|
-
end
|
290
|
-
|
291
|
-
def polymorphic_inverse_of(associated_class)
|
292
|
-
if has_inverse?
|
293
|
-
if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
|
294
|
-
inverse_relationship
|
295
|
-
else
|
296
|
-
raise InverseOfAssociationNotFoundError.new(self, associated_class)
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
# Returns whether or not this association reflection is for a collection
|
302
|
-
# association. Returns +true+ if the +macro+ is either +has_many+ or
|
303
|
-
# +has_and_belongs_to_many+, +false+ otherwise.
|
304
|
-
def collection?
|
305
|
-
@collection
|
306
|
-
end
|
307
|
-
|
308
|
-
# Returns whether or not the association should be validated as part of
|
309
|
-
# the parent's validation.
|
310
|
-
#
|
311
|
-
# Unless you explicitly disable validation with
|
312
|
-
# <tt>:validate => false</tt>, validation will take place when:
|
313
|
-
#
|
314
|
-
# * you explicitly enable validation; <tt>:validate => true</tt>
|
315
|
-
# * you use autosave; <tt>:autosave => true</tt>
|
316
|
-
# * the association is a +has_many+ association
|
317
|
-
def validate?
|
318
|
-
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
|
319
|
-
end
|
320
|
-
|
321
|
-
# Returns +true+ if +self+ is a +belongs_to+ reflection.
|
322
|
-
def belongs_to?
|
323
|
-
macro == :belongs_to
|
324
|
-
end
|
325
|
-
|
326
|
-
def association_class
|
327
|
-
case macro
|
328
|
-
when :belongs_to
|
329
|
-
if options[:polymorphic]
|
330
|
-
Associations::BelongsToPolymorphicAssociation
|
331
|
-
else
|
332
|
-
Associations::BelongsToAssociation
|
333
|
-
end
|
334
|
-
when :has_and_belongs_to_many
|
335
|
-
Associations::HasAndBelongsToManyAssociation
|
336
|
-
when :has_many
|
337
|
-
if options[:through]
|
338
|
-
Associations::HasManyThroughAssociation
|
339
|
-
else
|
340
|
-
Associations::HasManyAssociation
|
341
|
-
end
|
342
|
-
when :has_one
|
343
|
-
if options[:through]
|
344
|
-
Associations::HasOneThroughAssociation
|
345
|
-
else
|
346
|
-
Associations::HasOneAssociation
|
347
|
-
end
|
348
|
-
end
|
349
|
-
end
|
350
|
-
|
351
|
-
private
|
352
|
-
def derive_class_name
|
353
|
-
class_name = name.to_s.camelize
|
354
|
-
class_name = class_name.singularize if collection?
|
355
|
-
class_name
|
356
|
-
end
|
357
|
-
|
358
|
-
def derive_foreign_key
|
359
|
-
if belongs_to?
|
360
|
-
"#{name}_id"
|
361
|
-
elsif options[:as]
|
362
|
-
"#{options[:as]}_id"
|
363
|
-
else
|
364
|
-
active_record.name.foreign_key
|
365
|
-
end
|
366
|
-
end
|
367
|
-
|
368
|
-
def primary_key(klass)
|
369
|
-
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
|
370
|
-
end
|
371
|
-
end
|
143
|
+
#class AssociationReflection < MacroReflection #:nodoc: #NOTE totally overriden in Ooor
|
372
144
|
|
373
145
|
# Holds all the meta-data about a :through association as it was specified
|
374
146
|
# in the Active Record class.
|
375
|
-
class ThroughReflection < AssociationReflection #:nodoc:
|
376
|
-
# delegate :foreign_key, :foreign_type, :association_foreign_key, #FIXME hacked for OOOR
|
377
|
-
# :active_record_primary_key, :type, :to => :source_reflection
|
378
|
-
|
379
|
-
# Gets the source of the through reflection. It checks both a singularized
|
380
|
-
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
381
|
-
#
|
382
|
-
# class Post < ActiveRecord::Base
|
383
|
-
# has_many :taggings
|
384
|
-
# has_many :tags, :through => :taggings
|
385
|
-
# end
|
386
|
-
#
|
387
|
-
def source_reflection
|
388
|
-
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
389
|
-
end
|
390
|
-
|
391
|
-
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
|
392
|
-
# of a HasManyThrough or HasOneThrough association.
|
393
|
-
#
|
394
|
-
# class Post < ActiveRecord::Base
|
395
|
-
# has_many :taggings
|
396
|
-
# has_many :tags, :through => :taggings
|
397
|
-
# end
|
398
|
-
#
|
399
|
-
# tags_reflection = Post.reflect_on_association(:tags)
|
400
|
-
# taggings_reflection = tags_reflection.through_reflection
|
401
|
-
#
|
402
|
-
def through_reflection
|
403
|
-
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
404
|
-
end
|
405
|
-
|
406
|
-
# Returns an array of reflections which are involved in this association. Each item in the
|
407
|
-
# array corresponds to a table which will be part of the query for this association.
|
408
|
-
#
|
409
|
-
# The chain is built by recursively calling #chain on the source reflection and the through
|
410
|
-
# reflection. The base case for the recursion is a normal association, which just returns
|
411
|
-
# [self] as its #chain.
|
412
|
-
def chain
|
413
|
-
@chain ||= begin
|
414
|
-
chain = source_reflection.chain + through_reflection.chain
|
415
|
-
chain[0] = self # Use self so we don't lose the information from :source_type
|
416
|
-
chain
|
417
|
-
end
|
418
|
-
end
|
419
|
-
|
420
|
-
# Consider the following example:
|
421
|
-
#
|
422
|
-
# class Person
|
423
|
-
# has_many :articles
|
424
|
-
# has_many :comment_tags, :through => :articles
|
425
|
-
# end
|
426
|
-
#
|
427
|
-
# class Article
|
428
|
-
# has_many :comments
|
429
|
-
# has_many :comment_tags, :through => :comments, :source => :tags
|
430
|
-
# end
|
431
|
-
#
|
432
|
-
# class Comment
|
433
|
-
# has_many :tags
|
434
|
-
# end
|
435
|
-
#
|
436
|
-
# There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
|
437
|
-
# but only Comment.tags will be represented in the #chain. So this method creates an array
|
438
|
-
# of conditions corresponding to the chain. Each item in the #conditions array corresponds
|
439
|
-
# to an item in the #chain, and is itself an array of conditions from an arbitrary number
|
440
|
-
# of relevant reflections, plus any :source_type or polymorphic :as constraints.
|
441
|
-
def conditions
|
442
|
-
@conditions ||= begin
|
443
|
-
conditions = source_reflection.conditions.map { |c| c.dup }
|
444
|
-
|
445
|
-
# Add to it the conditions from this reflection if necessary.
|
446
|
-
conditions.first << options[:conditions] if options[:conditions]
|
447
|
-
|
448
|
-
through_conditions = through_reflection.conditions
|
449
|
-
|
450
|
-
if options[:source_type]
|
451
|
-
through_conditions.first << { foreign_type => options[:source_type] }
|
452
|
-
end
|
453
|
-
|
454
|
-
# Recursively fill out the rest of the array from the through reflection
|
455
|
-
conditions += through_conditions
|
456
|
-
|
457
|
-
# And return
|
458
|
-
conditions
|
459
|
-
end
|
460
|
-
end
|
461
|
-
|
462
|
-
# The macro used by the source association
|
463
|
-
def source_macro
|
464
|
-
source_reflection.source_macro
|
465
|
-
end
|
466
|
-
|
467
|
-
# A through association is nested if there would be more than one join table
|
468
|
-
def nested?
|
469
|
-
chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
|
470
|
-
end
|
471
|
-
|
472
|
-
# We want to use the klass from this reflection, rather than just delegate straight to
|
473
|
-
# the source_reflection, because the source_reflection may be polymorphic. We still
|
474
|
-
# need to respect the source_reflection's :primary_key option, though.
|
475
|
-
def association_primary_key(klass = nil)
|
476
|
-
# Get the "actual" source reflection if the immediate source reflection has a
|
477
|
-
# source reflection itself
|
478
|
-
source_reflection = self.source_reflection
|
479
|
-
while source_reflection.source_reflection
|
480
|
-
source_reflection = source_reflection.source_reflection
|
481
|
-
end
|
482
|
-
|
483
|
-
source_reflection.options[:primary_key] || primary_key(klass || self.klass)
|
484
|
-
end
|
485
|
-
|
486
|
-
# Gets an array of possible <tt>:through</tt> source reflection names:
|
487
|
-
#
|
488
|
-
# [:singularized, :pluralized]
|
489
|
-
#
|
490
|
-
def source_reflection_names
|
491
|
-
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
|
492
|
-
end
|
493
|
-
|
494
|
-
def source_options
|
495
|
-
source_reflection.options
|
496
|
-
end
|
497
|
-
|
498
|
-
def through_options
|
499
|
-
through_reflection.options
|
500
|
-
end
|
501
|
-
|
502
|
-
def check_validity!
|
503
|
-
if through_reflection.nil?
|
504
|
-
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
|
505
|
-
end
|
506
|
-
|
507
|
-
if through_reflection.options[:polymorphic]
|
508
|
-
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
|
509
|
-
end
|
510
|
-
|
511
|
-
if source_reflection.nil?
|
512
|
-
raise HasManyThroughSourceAssociationNotFoundError.new(self)
|
513
|
-
end
|
514
|
-
|
515
|
-
if options[:source_type] && source_reflection.options[:polymorphic].nil?
|
516
|
-
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
|
517
|
-
end
|
518
|
-
|
519
|
-
if source_reflection.options[:polymorphic] && options[:source_type].nil?
|
520
|
-
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
|
521
|
-
end
|
522
|
-
|
523
|
-
if macro == :has_one && through_reflection.collection?
|
524
|
-
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
|
525
|
-
end
|
526
|
-
|
527
|
-
check_validity_of_inverse!
|
528
|
-
end
|
529
|
-
|
530
|
-
private
|
531
|
-
def derive_class_name
|
532
|
-
# get the class_name of the belongs_to association of the through reflection
|
533
|
-
options[:source_type] || source_reflection.class_name
|
534
|
-
end
|
535
|
-
end
|
147
|
+
#class ThroughReflection < AssociationReflection #:nodoc: #NOTE commented out because not used in Ooor
|
536
148
|
end
|
537
149
|
end
|