ooor 1.9.2 → 2.0.0
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.
- data/README.md +23 -71
- data/Rakefile +5 -0
- data/bin/ooor +1 -0
- data/lib/ooor.rb +87 -129
- data/lib/ooor/associations.rb +64 -0
- data/lib/ooor/base.rb +218 -0
- data/lib/{app/models → ooor}/base64.rb +0 -0
- data/lib/ooor/connection.rb +37 -0
- data/lib/ooor/errors.rb +120 -0
- data/lib/ooor/field_methods.rb +153 -0
- data/lib/{app → ooor}/helpers/core_helpers.rb +2 -2
- data/lib/ooor/locale.rb +13 -0
- data/lib/ooor/mini_active_resource.rb +94 -0
- data/lib/ooor/model_registry.rb +19 -0
- data/lib/ooor/naming.rb +73 -0
- data/lib/ooor/rack.rb +114 -0
- data/lib/ooor/railtie.rb +41 -0
- data/lib/ooor/reflection.rb +537 -0
- data/lib/ooor/reflection_ooor.rb +92 -0
- data/lib/{app/models → ooor}/relation.rb +61 -22
- data/lib/ooor/relation/finder_methods.rb +113 -0
- data/lib/ooor/report.rb +53 -0
- data/lib/{app/models → ooor}/serialization.rb +18 -6
- data/lib/ooor/services.rb +133 -0
- data/lib/ooor/session.rb +120 -0
- data/lib/ooor/session_handler.rb +63 -0
- data/lib/ooor/transport.rb +34 -0
- data/lib/ooor/transport/json_client.rb +53 -0
- data/lib/ooor/transport/xml_rpc_client.rb +15 -0
- data/lib/ooor/type_casting.rb +193 -0
- data/lib/ooor/version.rb +8 -0
- data/spec/helpers/test_helper.rb +11 -0
- data/spec/install_nightly.sh +17 -0
- data/spec/ooor_spec.rb +197 -79
- data/spec/requirements.txt +19 -0
- metadata +58 -20
- data/lib/app/models/client_xmlrpc.rb +0 -34
- data/lib/app/models/open_object_resource.rb +0 -486
- data/lib/app/models/services.rb +0 -47
- data/lib/app/models/type_casting.rb +0 -134
- data/lib/app/models/uml.rb +0 -210
- data/lib/app/ui/action_window.rb +0 -96
- data/lib/app/ui/client_base.rb +0 -36
- data/lib/app/ui/form_model.rb +0 -88
- data/ooor.yml +0 -27
@@ -91,7 +91,7 @@ Ooor.xtend('ir.module.module') do
|
|
91
91
|
rescue
|
92
92
|
end
|
93
93
|
classes.reject! {|m| m.openerp_model == "res.company"} if classes.size > 10
|
94
|
-
|
94
|
+
OoorDoc::UML.print_uml(classes, {:file_name => "#{name}_uml"})
|
95
95
|
end
|
96
96
|
|
97
97
|
def print_dependency_graph
|
@@ -131,7 +131,7 @@ end
|
|
131
131
|
Ooor.xtend('ir.ui.menu') do
|
132
132
|
def menu_action
|
133
133
|
#TODO put in cache eventually:
|
134
|
-
action_values = self.class.ooor.const_get('ir.values').rpc_execute('get', 'action', 'tree_but_open', [['ir.ui.menu', id]], false, self.class.ooor.
|
134
|
+
action_values = self.class.ooor.const_get('ir.values').rpc_execute('get', 'action', 'tree_but_open', [['ir.ui.menu', id]], false, self.class.ooor.web_session)[0][2]#get already exists
|
135
135
|
@menu_action = self.class.ooor.const_get('ir.actions.act_window').new(action_values, []) #TODO deal with action reference instead
|
136
136
|
end
|
137
137
|
end
|
data/lib/ooor/locale.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module Ooor
|
2
|
+
module Locale
|
3
|
+
# OpenERP requirs a locale+zone mapping while Rails uses locale only, so mapping is likely to be required
|
4
|
+
def self.to_erp_locale(locale)
|
5
|
+
if Ooor.default_config[:locale_mapping]
|
6
|
+
mapping = Ooor.default_config[:locale_mapping]
|
7
|
+
else
|
8
|
+
mapping = {'fr' => 'fr_FR', 'en' => 'en_US'}
|
9
|
+
end
|
10
|
+
(mapping[locale.to_s] || locale.to_s).gsub('-', '_')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
module Ooor
|
6
|
+
class MiniActiveResource
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def element_name
|
10
|
+
@element_name ||= model_name.element
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
# split an option hash into two hashes, one containing the prefix options,
|
15
|
+
# and the other containing the leftovers.
|
16
|
+
def split_options(options = {})
|
17
|
+
prefix_options, query_options = {}, {}
|
18
|
+
|
19
|
+
(options || {}).each do |key, value|
|
20
|
+
next if key.blank? || !key.respond_to?(:to_sym)
|
21
|
+
query_options[key.to_sym] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
[ prefix_options, query_options ]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :attributes, :id
|
29
|
+
|
30
|
+
def to_json(options={})
|
31
|
+
super(include_root_in_json ? { :root => self.class.element_name }.merge(options) : options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_xml(options={})
|
35
|
+
super({ :root => self.class.element_name }.merge(options))
|
36
|
+
end
|
37
|
+
|
38
|
+
def new?
|
39
|
+
!@persisted
|
40
|
+
end
|
41
|
+
alias :new_record? :new?
|
42
|
+
|
43
|
+
def persisted?
|
44
|
+
@persisted
|
45
|
+
end
|
46
|
+
|
47
|
+
def id
|
48
|
+
attributes["id"]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets the <tt>\id</tt> attribute of the resource.
|
52
|
+
def id=(id)
|
53
|
+
attributes["id"] = id
|
54
|
+
end
|
55
|
+
|
56
|
+
def reload
|
57
|
+
self.class.find(id)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the Errors object that holds all information about attribute error messages.
|
61
|
+
def errors
|
62
|
+
@errors ||= ActiveModel::Errors.new(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def split_options(options = {})
|
68
|
+
self.class.__send__(:split_options, options)
|
69
|
+
end
|
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
|
+
include ActiveModel::Conversion
|
90
|
+
include ActiveModel::Serializers::JSON
|
91
|
+
include ActiveModel::Serializers::Xml
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Ooor
|
2
|
+
class ModelRegistry
|
3
|
+
|
4
|
+
def cache_key(config, model_name)
|
5
|
+
h = config.slice(:url, :database, :username, :scope_prefix) #sure we want username?
|
6
|
+
(h.map{|k, v| v} + [model_name]).join('-')
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_template(config, model_name)
|
10
|
+
Ooor.cache.read(cache_key(config, model_name))
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_template(config, model)
|
14
|
+
key = cache_key(config, model.openerp_model)
|
15
|
+
Ooor.cache.write(key, model)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/ooor/naming.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Ooor
|
4
|
+
module Naming
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def model_name
|
9
|
+
@_model_name ||= begin
|
10
|
+
namespace = self.parents.detect do |n|
|
11
|
+
n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
|
12
|
+
end
|
13
|
+
ActiveModel::Name.new(self, namespace, self.description).tap do |r|
|
14
|
+
def r.param_key
|
15
|
+
@klass.openerp_model.gsub('.', '_')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def param_key(context={})
|
22
|
+
self.alias(context).gsub('.', '-') # we don't use model_name because model_name isn't bijective
|
23
|
+
end
|
24
|
+
|
25
|
+
#similar to Object#const_get but for OpenERP model key
|
26
|
+
def const_get(model_key)
|
27
|
+
scope = self.scope_prefix ? Object.const_get(self.scope_prefix) : Object
|
28
|
+
klass_name = connection.class_name_from_model_key(model_key)
|
29
|
+
if scope.const_defined?(klass_name) && Ooor.session_handler.connection_spec(scope.const_get(klass_name).connection.config) == Ooor.session_handler.connection_spec(connection.config)
|
30
|
+
scope.const_get(klass_name)
|
31
|
+
else
|
32
|
+
connection.define_openerp_model(model: model_key, scope_prefix: self.scope_prefix)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#required by form validators; TODO implement better?
|
37
|
+
def human_attribute_name(field_name, options={})
|
38
|
+
""
|
39
|
+
end
|
40
|
+
|
41
|
+
def param_field
|
42
|
+
connection.config[:param_keys] && connection.config[:param_keys][openerp_model] || :id
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_by_permalink(param, options={})
|
46
|
+
# NOTE in v8, see if we can use PageConverter here https://github.com/akretion/openerp-addons/blob/trunk-website-al/website/models/ir_http.py#L138
|
47
|
+
param = param.to_i unless param.to_i == 0
|
48
|
+
options.merge!(domain: {param_field => param})
|
49
|
+
find(:first, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def alias(context={})
|
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
|
+
if connection.config[:aliases]
|
55
|
+
lang = context['lang'] || connection.config[:aliases][connection.config['lang'] || 'en_US']
|
56
|
+
if alias_data = connection.config[:aliases][lang]
|
57
|
+
alias_data.select{|key, value| value == openerp_model }.keys[0] || openerp_model
|
58
|
+
else
|
59
|
+
openerp_model
|
60
|
+
end
|
61
|
+
else
|
62
|
+
openerp_model
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_param
|
68
|
+
field = self.class.param_field
|
69
|
+
send(field) && send(field).to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
data/lib/ooor/rack.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Ooor
|
4
|
+
class Rack
|
5
|
+
|
6
|
+
DEFAULT_OOOR_SESSION_CONFIG_MAPPER = Proc.new do |env|
|
7
|
+
Ooor.logger.debug "\n\nWARNING: using DEFAULT_OOOR_SESSION_CONFIG_MAPPER, you should probably define your own instead!
|
8
|
+
You can define an Ooor::Rack.ooor_session_config_mapper block that will be evaled
|
9
|
+
in the context of the rack middleware call after user is authenticated using Warden.
|
10
|
+
Use it to map a Warden authentication to the OpenERP authentication you want.\n"""
|
11
|
+
Ooor.default_config
|
12
|
+
end
|
13
|
+
|
14
|
+
module RackBehaviour
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
module ClassMethods
|
17
|
+
def ooor_session_config_mapper(&block)
|
18
|
+
@ooor_session_config_mapper = block if block
|
19
|
+
@ooor_session_config_mapper || DEFAULT_OOOR_SESSION_CONFIG_MAPPER
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_ooor!(env)
|
24
|
+
ooor_session = self.get_ooor_session(env)
|
25
|
+
if defined?(I18n) && I18n.locale
|
26
|
+
lang = Ooor::Locale.to_erp_locale(I18n.locale)
|
27
|
+
elsif http_lang = env["HTTP_ACCEPT_LANGUAGE"]
|
28
|
+
lang = http_lang.split(',')[0].gsub('-', '_')
|
29
|
+
else
|
30
|
+
lang = ooor_session.config['lang'] || 'en_US'
|
31
|
+
end
|
32
|
+
context = {'lang' => lang} #TODO also deal with timezone
|
33
|
+
env['ooor'] = {'context' => context, 'ooor_session'=> ooor_session}
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_ooor_session(env)
|
37
|
+
cookies_hash = env['rack.request.cookie_hash'] || ::Rack::Request.new(env).cookies
|
38
|
+
session = Ooor.session_handler.sessions[cookies_hash['ooor_session_id']]
|
39
|
+
session ||= Ooor.session_handler.sessions[cookies_hash['session_id']]
|
40
|
+
unless session # session could have been used by an other worker, try getting it
|
41
|
+
config = Ooor::Rack.ooor_session_config_mapper.call(env)
|
42
|
+
spec = config[:session_sharing] ? cookies_hash['session_id'] : cookies_hash['ooor_session_id']
|
43
|
+
web_session = Ooor.session_handler.get_web_session(spec) if spec # created by some other worker?
|
44
|
+
unless web_session
|
45
|
+
if config[:session_sharing]
|
46
|
+
web_session = {session_id: cookies_hash['session_id']}
|
47
|
+
spec = cookies_hash['session_id']
|
48
|
+
else
|
49
|
+
web_session = {}
|
50
|
+
spec = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
session = Ooor.session_handler.retrieve_session(config, spec, web_session)
|
54
|
+
end
|
55
|
+
session
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_ooor_session!(env, status, headers, body)
|
59
|
+
case headers["Set-Cookie"]
|
60
|
+
when nil, ''
|
61
|
+
headers["Set-Cookie"] = ""
|
62
|
+
when Array
|
63
|
+
headers["Set-Cookie"] = headers["Set-Cookie"].join("\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
ooor_session = env['ooor']['ooor_session']
|
67
|
+
if ooor_session.config[:session_sharing]
|
68
|
+
share_openerp_session!(headers, ooor_session)
|
69
|
+
else # NOTE: we don't put that in a Rails session because we want to remain server agnostic
|
70
|
+
headers["Set-Cookie"] = [headers["Set-Cookie"],
|
71
|
+
"ooor_session_id=#{ooor_session.id}; Path=/",
|
72
|
+
].join("\n")
|
73
|
+
end
|
74
|
+
response = ::Rack::Response.new body, status, headers
|
75
|
+
response.finish
|
76
|
+
end
|
77
|
+
|
78
|
+
def share_openerp_session!(headers, ooor_session)
|
79
|
+
if ooor_session.config[:username] == 'admin'
|
80
|
+
if ooor_session.config[:force_session_sharing]
|
81
|
+
Ooor.logger.debug "Warning! force_session_sharing mode with admin user, this may be a serious security breach! Are you really in development mode?"
|
82
|
+
else
|
83
|
+
raise "Sharing OpenERP session for admin user is suicidal (use force_session_sharing in dev mode and be paranoiac about it)"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
cookie = ooor_session.web_session[:cookie]
|
87
|
+
headers["Set-Cookie"] = [headers["Set-Cookie"], cookie].join("\n")
|
88
|
+
|
89
|
+
if ooor_session.web_session[:sid] #v7
|
90
|
+
session_id = ooor_session.web_session[:session_id]
|
91
|
+
headers["Set-Cookie"] = [headers["Set-Cookie"],
|
92
|
+
"instance0|session_id=%22#{session_id}%22; Path=/",
|
93
|
+
"last_used_database=#{ooor_session.config[:database]}; Path=/",
|
94
|
+
"session_id=#{session_id}; Path=/",
|
95
|
+
].join("\n")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
include RackBehaviour
|
102
|
+
|
103
|
+
def initialize(app=nil)
|
104
|
+
@app=app
|
105
|
+
end
|
106
|
+
|
107
|
+
def call(env)
|
108
|
+
set_ooor!(env)
|
109
|
+
status, headers, body = @app.call(env)
|
110
|
+
set_ooor_session!(env, status, headers, body)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
data/lib/ooor/railtie.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "rails/railtie"
|
2
|
+
require "ooor/rack"
|
3
|
+
|
4
|
+
module Ooor
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
initializer "ooor.middleware" do |app|
|
7
|
+
Ooor.logger = Rails.logger unless $0 != 'irb'
|
8
|
+
Ooor.default_config = load_config(false, Rails.env)
|
9
|
+
Ooor.logger.level = @config[:log_level] if @config[:log_level]
|
10
|
+
Ooor.cache_store = Rails.cache
|
11
|
+
Ooor.default_session = Ooor.session_handler.retrieve_session(Ooor.default_config)
|
12
|
+
|
13
|
+
if Ooor.default_config[:bootstrap]
|
14
|
+
Ooor.default_session.global_login(config.merge(generate_constants: true))
|
15
|
+
end
|
16
|
+
unless Ooor.default_config[:disable_locale_switcher]
|
17
|
+
if defined?(Rack::I18nLocaleSwitcher)
|
18
|
+
app.middleware.use '::Rack::I18nLocaleSwitcher'
|
19
|
+
else
|
20
|
+
puts "Could not load Rack::I18nLocaleSwitcher, if your application is internationalized, make sure to include rack-i18n_locale_switcher in your Gemfile"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
if defined?(Warden::Manager)
|
24
|
+
app.middleware.insert_after Warden::Manager, '::Ooor::Rack'
|
25
|
+
else
|
26
|
+
app.middleware.insert_after ActionDispatch::ParamsParser, '::Ooor::Rack'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_config(config_file=nil, env=nil)
|
31
|
+
config_file ||= defined?(Rails.root) && "#{Rails.root}/config/ooor.yml" || 'ooor.yml'
|
32
|
+
@config = HashWithIndifferentAccess.new(YAML.load_file(config_file)[env || 'development'])
|
33
|
+
rescue SystemCallError
|
34
|
+
Ooor.logger.error """failed to load OOOR yaml configuration file.
|
35
|
+
make sure your app has a #{config_file} file correctly set up
|
36
|
+
if not, just copy/paste the default ooor.yml file from the OOOR Gem
|
37
|
+
to #{Rails.root}/config/ooor.yml and customize it properly\n\n"""
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,537 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
require 'active_support/core_ext/object/inclusion'
|
3
|
+
|
4
|
+
# NOTE this is a scoped copy of ActiveRecord reflection.rb
|
5
|
+
# the few necessary hacks are explicited with a FIXME
|
6
|
+
# an addition Ooor specific reflection module completes this one explicitely
|
7
|
+
module Ooor
|
8
|
+
# = Active Record Reflection
|
9
|
+
module Reflection # :nodoc:
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
class_attribute :reflections
|
14
|
+
self.reflections = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Reflection enables to interrogate Active Record classes and objects
|
18
|
+
# about their associations and aggregations. This information can,
|
19
|
+
# for example, be used in a form builder that takes an Active Record object
|
20
|
+
# and creates input fields for all of the attributes depending on their type
|
21
|
+
# and displays the associations to other objects.
|
22
|
+
#
|
23
|
+
# MacroReflection class has info for AggregateReflection and AssociationReflection
|
24
|
+
# classes.
|
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
|
38
|
+
|
39
|
+
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
40
|
+
def reflect_on_all_aggregations
|
41
|
+
reflections.values.grep(AggregateReflection)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
|
45
|
+
#
|
46
|
+
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
|
47
|
+
#
|
48
|
+
def reflect_on_aggregation(aggregation)
|
49
|
+
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns an array of AssociationReflection objects for all the
|
53
|
+
# associations in the class. If you only want to reflect on a certain
|
54
|
+
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
|
55
|
+
# <tt>:belongs_to</tt>) as the first parameter.
|
56
|
+
#
|
57
|
+
# Example:
|
58
|
+
#
|
59
|
+
# Account.reflect_on_all_associations # returns an array of all associations
|
60
|
+
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
|
61
|
+
#
|
62
|
+
def reflect_on_all_associations(macro = nil)
|
63
|
+
association_reflections = reflections.values.grep(AssociationReflection)
|
64
|
+
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the AssociationReflection object for the +association+ (use the symbol).
|
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
|
75
|
+
|
76
|
+
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
77
|
+
def reflect_on_all_autosave_associations
|
78
|
+
reflections.values.select { |reflection| reflection.options[:autosave] }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# Abstract base class for AggregateReflection and AssociationReflection. Objects of
|
84
|
+
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
|
85
|
+
class MacroReflection
|
86
|
+
# Returns the name of the macro.
|
87
|
+
#
|
88
|
+
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
|
89
|
+
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
|
90
|
+
attr_reader :name
|
91
|
+
|
92
|
+
# Returns the macro type.
|
93
|
+
#
|
94
|
+
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt>
|
95
|
+
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
|
96
|
+
attr_reader :macro
|
97
|
+
|
98
|
+
# Returns the hash of options used for the macro.
|
99
|
+
#
|
100
|
+
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
|
101
|
+
# <tt>has_many :clients</tt> returns +{}+
|
102
|
+
attr_reader :options
|
103
|
+
|
104
|
+
attr_reader :active_record
|
105
|
+
|
106
|
+
attr_reader :plural_name # :nodoc:
|
107
|
+
|
108
|
+
def initialize(macro, name, options, active_record)
|
109
|
+
@macro = macro
|
110
|
+
@name = name
|
111
|
+
@options = options
|
112
|
+
@active_record = active_record
|
113
|
+
# @plural_name = active_record.pluralize_table_names ? #FIXME hacked for OOOR
|
114
|
+
# name.to_s.pluralize : name.to_s
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the class for the macro.
|
118
|
+
#
|
119
|
+
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
|
120
|
+
# <tt>has_many :clients</tt> returns the Client class
|
121
|
+
def klass
|
122
|
+
@klass ||= class_name.constantize
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns the class name for the macro.
|
126
|
+
#
|
127
|
+
# <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
|
128
|
+
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
|
129
|
+
def class_name
|
130
|
+
@class_name ||= (options[:class_name] || derive_class_name).to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
|
134
|
+
# and +other_aggregation+ has an options hash assigned to it.
|
135
|
+
def ==(other_aggregation)
|
136
|
+
super ||
|
137
|
+
other_aggregation.kind_of?(self.class) &&
|
138
|
+
name == other_aggregation.name &&
|
139
|
+
other_aggregation.options &&
|
140
|
+
active_record == other_aggregation.active_record
|
141
|
+
end
|
142
|
+
|
143
|
+
def sanitized_conditions #:nodoc:
|
144
|
+
@sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def derive_class_name
|
149
|
+
name.to_s.camelize
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Holds all the meta-data about an aggregation as it was specified in the
|
155
|
+
# Active Record class.
|
156
|
+
class AggregateReflection < MacroReflection #:nodoc:
|
157
|
+
end
|
158
|
+
|
159
|
+
# Holds all the meta-data about an association as it was specified in the
|
160
|
+
# 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
|
372
|
+
|
373
|
+
# Holds all the meta-data about a :through association as it was specified
|
374
|
+
# 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
|
536
|
+
end
|
537
|
+
end
|