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.
Files changed (45) hide show
  1. data/README.md +23 -71
  2. data/Rakefile +5 -0
  3. data/bin/ooor +1 -0
  4. data/lib/ooor.rb +87 -129
  5. data/lib/ooor/associations.rb +64 -0
  6. data/lib/ooor/base.rb +218 -0
  7. data/lib/{app/models → ooor}/base64.rb +0 -0
  8. data/lib/ooor/connection.rb +37 -0
  9. data/lib/ooor/errors.rb +120 -0
  10. data/lib/ooor/field_methods.rb +153 -0
  11. data/lib/{app → ooor}/helpers/core_helpers.rb +2 -2
  12. data/lib/ooor/locale.rb +13 -0
  13. data/lib/ooor/mini_active_resource.rb +94 -0
  14. data/lib/ooor/model_registry.rb +19 -0
  15. data/lib/ooor/naming.rb +73 -0
  16. data/lib/ooor/rack.rb +114 -0
  17. data/lib/ooor/railtie.rb +41 -0
  18. data/lib/ooor/reflection.rb +537 -0
  19. data/lib/ooor/reflection_ooor.rb +92 -0
  20. data/lib/{app/models → ooor}/relation.rb +61 -22
  21. data/lib/ooor/relation/finder_methods.rb +113 -0
  22. data/lib/ooor/report.rb +53 -0
  23. data/lib/{app/models → ooor}/serialization.rb +18 -6
  24. data/lib/ooor/services.rb +133 -0
  25. data/lib/ooor/session.rb +120 -0
  26. data/lib/ooor/session_handler.rb +63 -0
  27. data/lib/ooor/transport.rb +34 -0
  28. data/lib/ooor/transport/json_client.rb +53 -0
  29. data/lib/ooor/transport/xml_rpc_client.rb +15 -0
  30. data/lib/ooor/type_casting.rb +193 -0
  31. data/lib/ooor/version.rb +8 -0
  32. data/spec/helpers/test_helper.rb +11 -0
  33. data/spec/install_nightly.sh +17 -0
  34. data/spec/ooor_spec.rb +197 -79
  35. data/spec/requirements.txt +19 -0
  36. metadata +58 -20
  37. data/lib/app/models/client_xmlrpc.rb +0 -34
  38. data/lib/app/models/open_object_resource.rb +0 -486
  39. data/lib/app/models/services.rb +0 -47
  40. data/lib/app/models/type_casting.rb +0 -134
  41. data/lib/app/models/uml.rb +0 -210
  42. data/lib/app/ui/action_window.rb +0 -96
  43. data/lib/app/ui/client_base.rb +0 -36
  44. data/lib/app/ui/form_model.rb +0 -88
  45. 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
- Ooor::UML.print_uml(classes, {:file_name => "#{name}_uml"})
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.global_context)[0][2]#get already exists
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
@@ -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
@@ -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
@@ -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