ooor 2.0.3 → 2.0.4

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c7013e218cfc09bfb4c0711c65904076f14f6e9d
4
+ data.tar.gz: 37e409fe1330084c5ada2b5051c92381577febe1
5
+ SHA512:
6
+ metadata.gz: 09af180037d24d0008ef92c4365fa81ca9fbb503b8f07045cb1fd18e134785ddb72b8aff170c9a2c51e136b0422b6e630bef2f534f038702b0821de92254060c
7
+ data.tar.gz: 2330653e660fd06751bc4bfd1ab79bfe9107b37ae441a2a98bb80ee3a85839046cd14146b4d86fa7bcf9e7d5b5cb5c714b70a7cf34fd08d120a0980d929c847c
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  [![Build Status](https://secure.travis-ci.org/akretion/ooor.png?branch=master)](http://travis-ci.org/akretion/ooor) [![Code Climate](https://codeclimate.com/github/akretion/ooor.png)](https://codeclimate.com/github/akretion/ooor)
2
+ [![Coverage Status](https://coveralls.io/repos/akretion/ooor/badge.png?branch=master)](https://coveralls.io/r/akretion/ooor?branch=master) [![Gem Version](https://badge.fury.io/rb/ooor.png)](http://badge.fury.io/rb/ooor) [![Dependency Status](https://www.versioneye.com/ruby/ooor/badge.png)](https://www.versioneye.com/ruby/ooor)
2
3
 
3
4
  [![OOOR by Akretion](https://s3.amazonaws.com/akretion/assets/ooor_by_akretion.png)](http://akretion.com)
4
5
 
data/bin/ooor CHANGED
@@ -41,6 +41,7 @@ unless ARGV.empty?
41
41
  system "stty -echo"
42
42
  password = $stdin.gets.chomp!
43
43
  system "stty echo"
44
+ password = 'admin' if password.blank?
44
45
  end
45
46
 
46
47
  else
@@ -12,6 +12,8 @@ require 'logger'
12
12
  module Ooor
13
13
  extend ActiveSupport::Autoload
14
14
  autoload :Base
15
+ autoload :Persistence
16
+ autoload :Callbacks
15
17
  autoload :Cache, 'active_support/cache'
16
18
  autoload :Serialization
17
19
  autoload :Relation
@@ -1,63 +1,53 @@
1
1
  module Ooor
2
2
  module Associations
3
3
 
4
- def many2one_id_method(rel, *arguments)
5
- if @associations[rel]
6
- @associations[rel][0]
7
- else
8
- obj = method_missing(rel.to_sym, *arguments)
9
- obj.is_a?(Base) ? obj.id : obj
4
+ # similar to ActiveRecord CollectionProxy but without lazy loading work yet
5
+ class CollectionProxy < Relation
6
+
7
+ def to_ary
8
+ to_a.dup
9
+ end
10
+ # alias_method :to_a, :to_ary
11
+
12
+ def class
13
+ Array
10
14
  end
11
- end
12
15
 
13
- def x_to_many_ids_method(rel, *arguments)
14
- if @associations[rel]
15
- @associations[rel]
16
- else
17
- method_missing(rel.to_sym, *arguments)
16
+ def is_a?(*args)
17
+ @records.is_a?(*args)
18
18
  end
19
+
20
+ def kind_of?(*args)
21
+ @records.kind_of?(*args)
22
+ end
23
+
19
24
  end
20
25
 
26
+
21
27
  # fakes associations like much like ActiveRecord according to the cached OpenERP data model
22
28
  def relationnal_result(method_name, *arguments)
23
29
  self.class.reload_fields_definition(false, object_session)
24
30
  if self.class.many2one_associations.has_key?(method_name)
25
- if @associations[method_name]
26
- rel = self.class.many2one_associations[method_name]['relation']
27
- id = @associations[method_name].is_a?(Integer) ? @associations[method_name] : @associations[method_name][0]
28
- load_association(rel, id, nil, *arguments)
31
+ if !@associations[method_name]
32
+ nil
29
33
  else
30
- false
34
+ id = @associations[method_name].is_a?(Integer) ? @associations[method_name] : @associations[method_name][0]
35
+ rel = self.class.many2one_associations[method_name]['relation']
36
+ self.class.const_get(rel).find(id, arguments.extract_options!)
31
37
  end
32
- elsif self.class.one2many_associations.has_key?(method_name)
33
- rel = self.class.one2many_associations[method_name]['relation']
34
- load_association(rel, @associations[method_name], [], *arguments)
35
- elsif self.class.many2many_associations.has_key?(method_name)
36
- rel = self.class.many2many_associations[method_name]['relation']
37
- load_association(rel, @associations[method_name], [], *arguments)
38
- elsif self.class.polymorphic_m2o_associations.has_key?(method_name)
38
+ elsif self.class.polymorphic_m2o_associations.has_key?(method_name) && @associations[method_name]
39
39
  values = @associations[method_name].split(',')
40
- load_association(values[0], values[1].to_i, nil, *arguments)
41
- else
42
- false
40
+ self.class.const_get(values[0]).find(values[1], arguments.extract_options!)
41
+ else # o2m or m2m
42
+ rel = self.class.all_fields[method_name]['relation']
43
+ load_x2m_association(rel, @associations[method_name], *arguments)
43
44
  end
44
45
  end
45
46
 
46
- def load_association(model_key, ids, substitute=nil, *arguments)
47
+ def load_x2m_association(model_key, ids, *arguments)
47
48
  options = arguments.extract_options!
48
49
  related_class = self.class.const_get(model_key)
49
- fields = options[:fields] || options[:only] || nil
50
- context = options[:context] || object_session
51
- (related_class.send(:find, ids, fields: fields, context: context) || substitute).tap do |r|
52
- #TODO the following is a hack to minimally mimic the CollectionProxy of Rails 3.1+; this should probably be re-implemented
53
- def r.association=(association)
54
- @association = association
55
- end
56
- r.association = related_class
57
- def r.build(attrs={})
58
- @association.new(attrs)
59
- end
60
- end
50
+ CollectionProxy.new(related_class, {}).apply_finder_options(options.merge(ids: ids))
61
51
  end
62
52
 
63
53
  end
@@ -1,27 +1,34 @@
1
1
  # OOOR: OpenObject On Ruby
2
- # Copyright (C) 2009-2013 Akretion LTDA (<http://www.akretion.com>).
2
+ # Copyright (C) 2009-2014 Akretion LTDA (<http://www.akretion.com>).
3
3
  # Author: Raphaël Valyi
4
4
  # Licensed under the MIT license, see MIT-LICENSE file
5
5
 
6
- require 'active_support/core_ext/hash/indifferent_access'
7
6
  require 'active_support/core_ext/module/delegation.rb'
7
+ require 'active_model/attribute_methods'
8
+ require 'active_model/dirty'
8
9
  require 'ooor/reflection'
9
10
  require 'ooor/reflection_ooor'
11
+ require 'ooor/errors'
10
12
 
11
13
  module Ooor
12
- class ModelTemplate #meta data shared across sessions
13
- TEMPLATE_PROPERTIES = [:openerp_id, :info, :access_ids, :description,
14
+
15
+ # meta data shared across sessions, a cache of the data in ir_model in OpenERP.
16
+ # reused accross workers in a multi-process web app (via memcache for instance).
17
+ class ModelTemplate
18
+
19
+ TEMPLATE_PROPERTIES = [:name, :openerp_id, :info, :access_ids, :description,
14
20
  :openerp_model, :field_ids, :state, :fields,
15
- :many2one_associations, :one2many_associations, :many2many_associations, :polymorphic_m2o_associations, :associations_keys,
16
- :associations, :columns, :columns_hash]
17
- attr_accessor *TEMPLATE_PROPERTIES
18
- end
21
+ :many2one_associations, :one2many_associations, :many2many_associations,
22
+ :polymorphic_m2o_associations, :associations_keys,
23
+ :associations, :columns]
19
24
 
25
+ attr_accessor *TEMPLATE_PROPERTIES, :columns_hash
26
+ end
20
27
 
28
+ # the base class for proxies to OpenERP objects
21
29
  class Base < Ooor::MiniActiveResource
22
- #PREDEFINED_INHERITS = {'product.product' => 'product_tmpl_id'}
23
- include Naming, TypeCasting, Serialization, ReflectionOoor, Reflection, Associations, Report, FinderMethods, FieldMethods
24
-
30
+ include Naming, TypeCasting, Serialization, ReflectionOoor, Reflection
31
+ include Associations, Report, FinderMethods, FieldMethods
25
32
 
26
33
  # ********************** class methods ************************************
27
34
  class << self
@@ -71,6 +78,8 @@ module Ooor
71
78
  def limit(value); relation.limit(value); end
72
79
  def order(value); relation.order(value); end
73
80
  def offset(value); relation.offset(value); end
81
+ def first(*args); relation.first(*args); end
82
+ def last(*args); relation.last(*args); end
74
83
 
75
84
  def logger; Ooor.logger; end
76
85
 
@@ -82,90 +91,13 @@ module Ooor
82
91
 
83
92
  attr_accessor :associations, :loaded_associations, :ir_model_data_id, :object_session
84
93
 
94
+ include Persistence, Callbacks, ActiveModel::Dirty
95
+
85
96
  def rpc_execute(method, *args)
86
97
  args += [self.class.connection.connection_session.merge(object_session)] unless args[-1].is_a? Hash
87
98
  self.class.object_service(:execute, self.class.openerp_model, method, *args)
88
99
  end
89
100
 
90
- def load(attributes, remove_root=false, persisted=false)#an attribute might actually be a association too, will be determined here
91
- self.class.reload_fields_definition(false, object_session)
92
- raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
93
- @prefix_options, attributes = split_options(attributes)
94
- @associations ||= {}
95
- @attributes ||= {}
96
- @loaded_associations = {}
97
- attributes.each do |key, value|
98
- skey = key.to_s
99
- if self.class.associations_keys.index(skey) || value.is_a?(Array) #FIXME may miss m2o with inherits!
100
- if value.is_a?(Ooor::Base) || value.is_a?(Array) && value.all? {|i| i.is_a?(Ooor::Base)}
101
- @loaded_associations[skey] = value #we want the method to load the association through method missing
102
- else
103
- @associations[skey] = value
104
- end
105
- else
106
- @attributes[skey] = value || nil #don't bloat with false values
107
- end
108
- end
109
- self
110
- end
111
-
112
- #takes care of reading OpenERP default field values.
113
- def initialize(attributes = {}, default_get_list=false, context={}, persisted=false)
114
- @attributes = {}
115
- @prefix_options = {}
116
- @ir_model_data_id = attributes.delete(:ir_model_data_id)
117
- @object_session = {}
118
- @object_session = HashWithIndifferentAccess.new(context)
119
- @persisted = persisted
120
- self.class.reload_fields_definition(false, @object_session)
121
- if default_get_list == []
122
- load(attributes)
123
- else
124
- load_with_defaults(attributes, default_get_list)
125
- end
126
- end
127
-
128
- def save(context={}, reload=true)
129
- new? ? create(context, reload) : update(context, reload)
130
- rescue ValidationError => e
131
- e.extract_validation_error!(errors)
132
- return false
133
- end
134
-
135
- #compatible with the Rails way but also supports OpenERP context
136
- def create(context={}, reload=true)
137
- self.id = rpc_execute('create', to_openerp_hash, context)
138
- if @ir_model_data_id
139
- IrModelData.create(model: self.class.openerp_model,
140
- 'module' => @ir_model_data_id[0],
141
- 'name' => @ir_model_data_id[1],
142
- 'res_id' => self.id)
143
- end
144
- reload_from_record!(self.class.find(self.id, context: context)) if reload
145
- @persisted = true
146
- end
147
-
148
- def update_attributes(attributes, context={}, reload=true)
149
- load(attributes, false) && save(context, reload)
150
- end
151
-
152
- #compatible with the Rails way but also supports OpenERP context
153
- def update(context={}, reload=true) #TODO use http://apidock.com/rails/ActiveRecord/Dirty to minimize data to save back
154
- rpc_execute('write', [self.id], to_openerp_hash, context)
155
- reload_fields(context) if reload
156
- @persisted = true
157
- end
158
-
159
- #compatible with the Rails way but also supports OpenERP context
160
- def destroy(context={})
161
- rpc_execute('unlink', [self.id], context)
162
- end
163
-
164
- #OpenERP copy method, load persisted copied Object
165
- def copy(defaults={}, context={})
166
- self.class.find(rpc_execute('copy', self.id, defaults, context), context: context)
167
- end
168
-
169
101
  #Generic OpenERP rpc method call
170
102
  def call(method, *args) rpc_execute(method, *args) end
171
103
 
@@ -192,31 +124,8 @@ module Ooor
192
124
 
193
125
  private
194
126
 
195
- def load_with_defaults(attributes, default_get_list)
196
- defaults = rpc_execute("default_get", default_get_list || self.class.fields.keys + self.class.associations_keys, object_session.dup)
197
- attributes = HashWithIndifferentAccess.new(defaults.merge(attributes.reject {|k, v| v.blank? }))
198
- load(attributes)
199
- end
200
-
201
- def load_on_change_result(result, field_name, field_value)
202
- if result["warning"]
203
- self.class.logger.info result["warning"]["title"]
204
- self.class.logger.info result["warning"]["message"]
205
- end
206
- attrs = @attributes.merge(field_name => field_value)
207
- attrs.merge!(result["value"])
208
- load(attrs)
209
- end
210
-
211
127
  # Ruby 1.9.compat, See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
212
128
  def to_ary; nil; end # :nodoc:
213
129
 
214
- def reload_from_record!(record) load(record.attributes.merge(record.associations)) end
215
-
216
- def reload_fields(context)
217
- records = self.class.find(self.id, context: context, fields: @attributes.keys + @associations.keys)
218
- reload_from_record!(records)
219
- end
220
-
221
130
  end
222
131
  end
@@ -0,0 +1,18 @@
1
+ module Ooor
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ CALLBACKS = [
6
+ :before_validation, :after_validation, :before_save, :around_save, :after_save,
7
+ :before_create, :around_create, :after_create, :before_update, :around_update,
8
+ :after_update, :before_destroy, :around_destroy, :after_destroy
9
+ ]
10
+
11
+ included do
12
+ extend ActiveModel::Callbacks
13
+ include ActiveModel::Validations::Callbacks
14
+
15
+ define_model_callbacks :save, :create, :update, :destroy
16
+ end
17
+ end
18
+ end
@@ -13,22 +13,17 @@ module Ooor
13
13
  fields_get = rpc_execute("fields_get", false, context)
14
14
  fields_get.each { |k, field| reload_field_definition(k, field) }
15
15
  @t.associations_keys = many2one_associations.keys + one2many_associations.keys + many2many_associations.keys + polymorphic_m2o_associations.keys
16
- (fields.keys + associations_keys).each do |meth| #generates method handlers for auto-completion tools
17
- define_field_method(meth)
18
- end
19
- one2many_associations.keys.each do |meth|
20
- define_nested_attributes_method(meth)
21
- end
22
16
  logger.debug "#{fields.size} fields loaded in model #{self.name}"
23
17
  Ooor.model_registry.set_template(connection.config, @t)
24
18
  end
19
+ generate_accessors if fields != {} && (force || !@accessor_defined) #TODOmove in define_accessors method
25
20
  end
26
21
 
27
22
  def all_fields
28
23
  fields.merge(polymorphic_m2o_associations).merge(many2many_associations).merge(one2many_associations).merge(many2one_associations)
29
24
  end
30
25
 
31
- def fast_fields(options)
26
+ def fast_fields(options={})
32
27
  fields = all_fields
33
28
  fields.keys.select do |k|
34
29
  fields[k]["type"] != "binary" && (options[:include_functions] || !fields[k]["function"])
@@ -40,10 +35,12 @@ module Ooor
40
35
  unless self.respond_to?(meth)
41
36
  self.instance_eval do
42
37
  define_method "#{meth}_attributes=" do |*args|
43
- self.send :method_missing, *[meth, *args]
38
+ send("#{meth}_will_change!")
39
+ @associations[meth] = args[0]
40
+ @loaded_associations[meth] = args[0]
44
41
  end
45
42
  define_method "#{meth}_attributes" do |*args|
46
- self.send :method_missing, *[meth, *args]
43
+ @loaded_associations[meth]
47
44
  end
48
45
  end
49
46
  end
@@ -51,102 +48,153 @@ module Ooor
51
48
 
52
49
  private
53
50
 
54
- def define_field_method(meth)
55
- unless self.respond_to?(meth)
56
- self.instance_eval do
57
- define_method meth do |*args|
58
- self.send :method_missing, *[meth, *args]
59
- end
60
- end
61
- end
51
+ def generate_accessors #TODO we should cache this is a module cached like the template, or eventually generate source code or both
52
+ fields.keys.each { |meth| define_field_method meth }
53
+ associations_keys.each { |meth| define_association_method meth }
54
+ one2many_associations.keys.each { |meth| define_nested_attributes_method meth }
55
+ many2one_associations.keys.each do |meth|
56
+ define_association_method meth
57
+ define_m2o_association_method meth
62
58
  end
59
+ (one2many_associations.keys + many2many_associations.keys).each do |meth|
60
+ define_association_method meth
61
+ alias_method "#{meth}_ids", meth
62
+ alias_method "#{meth}_ids=", "#{meth}="
63
+ alias_method "#{meth.to_s.singularize}_ids", meth
64
+ alias_method "#{meth.to_s.singularize}_ids=", "#{meth}="
65
+ end
66
+ @accessor_defined = true
67
+ end
63
68
 
64
- def reload_field_definition(k, field)
65
- case field['type']
66
- when 'many2one'
67
- many2one_associations[k] = field
68
- when 'one2many'
69
- one2many_associations[k] = field
70
- when 'many2many'
71
- many2many_associations[k] = field
72
- when 'reference'
73
- polymorphic_m2o_associations[k] = field
74
- else
75
- fields[k] = field if field['name'] != 'id'
76
- end
69
+ def define_field_method(meth)
70
+ define_attribute_methods meth
71
+ define_method meth do |*args|
72
+ get_attribute(meth, *args)
77
73
  end
78
- end
79
74
 
80
- def method_missing(method_symbol, *arguments)
81
- method_name = method_symbol.to_s
82
- method_key = method_name.sub('=', '')
83
- self.class.reload_fields_definition(false, object_session)
84
- if attributes.has_key?(method_key)
85
- if method_name.end_with?('=')
86
- attributes[method_key] = arguments[0]
87
- else
88
- attributes[method_key]
75
+ define_method "#{meth}=" do |*args|
76
+ set_attribute(meth, *args)
89
77
  end
90
- elsif @loaded_associations.has_key?(method_name)
91
- @loaded_associations[method_name]
92
- elsif @associations.has_key?(method_name)
93
- result = relationnal_result(method_name, *arguments)
94
- @loaded_associations[method_name] = result and return result if result
95
- elsif method_name.end_with?('=')
96
- return method_missing_value_assign(method_key, arguments)
97
- elsif self.class.fields.has_key?(method_name) || self.class.associations_keys.index(method_name) #unloaded field/association
98
- return lazzy_load_field(method_name, *arguments)
99
- # check if that is not a Rails style association with an _id[s][=] suffix:
100
- elsif method_name.match(/_id$/) && self.class.associations_keys.index(rel=method_name.gsub(/_id$/, ""))
101
- return many2one_id_method(rel, *arguments)
102
- elsif method_name.match(/_ids$/) && self.class.associations_keys.index(rel=method_name.gsub(/_ids$/, ""))
103
- return x_to_many_ids_method(rel, *arguments)
104
- elsif id
105
- rpc_execute(method_key, [id], *arguments) #we assume that's an action
106
- else
107
- super
108
78
  end
109
79
 
110
- rescue UnknownAttributeOrAssociationError => e
111
- e.klass = self.class
112
- raise e
113
- end
80
+ def define_association_method(meth)
81
+ define_attribute_methods meth
82
+ define_method meth do |*args|
83
+ get_association(meth, *args)
84
+ end
114
85
 
115
- private
86
+ define_method "#{meth}=" do |*args|
87
+ set_association(meth, *args)
88
+ end
89
+ end
116
90
 
117
- def method_missing_value_assign(method_key, arguments)
118
- if is_association_assignment(method_key)
119
- @associations[method_key] = arguments[0]
120
- @loaded_associations[method_key] = arguments[0]
121
- elsif is_attribute_assignment(method_key)
122
- @attributes[method_key] = arguments[0]
91
+ def define_m2o_association_method(meth)
92
+ define_method "#{meth}_id" do |*args|
93
+ if @associations[meth].is_a? Array
94
+ @associations[meth][0]
95
+ else
96
+ r = get_association(meth, *args)
97
+ r.is_a?(Ooor::Base) ? r.id : r
98
+ end
123
99
  end
124
100
  end
125
101
 
126
- def is_association_assignment(method_key)
127
- (self.class.associations_keys + self.class.many2one_associations.collect do |k, field|
128
- klass = self.class.const_get(field['relation'])
129
- klass.reload_fields_definition(false, object_session)
130
- klass.t.associations_keys
131
- end.flatten).index(method_key)
102
+ def reload_field_definition(k, field)
103
+ case field['type']
104
+ when 'many2one'
105
+ many2one_associations[k] = field
106
+ when 'one2many'
107
+ one2many_associations[k] = field
108
+ when 'many2many'
109
+ many2many_associations[k] = field
110
+ when 'reference'
111
+ polymorphic_m2o_associations[k] = field
112
+ else
113
+ fields[k] = field if field['name'] != 'id'
114
+ end
132
115
  end
133
116
 
134
- def is_attribute_assignment(method_key)
135
- (self.class.fields.keys + self.class.many2one_associations.collect do |k, field|
136
- klass = self.class.const_get(field['relation'])
137
- klass.reload_fields_definition(false, object_session)
138
- klass.t.fields.keys
139
- end.flatten).index(method_key)
117
+ end
118
+
119
+ def get_attribute(meth, *args)
120
+ if @attributes.has_key?(meth)
121
+ @attributes[meth]
122
+ else #lazy loading
123
+ if @attributes["id"]
124
+ @attributes[meth] = rpc_execute('read', [@attributes["id"]], [meth], *args || object_session)[0][meth]
125
+ else
126
+ nil
127
+ end
140
128
  end
129
+ end
130
+
131
+ def set_attribute(meth, *args)
132
+ value = sanitize_attribute(meth, args[0])
133
+ @attributes[meth] ||= nil
134
+ send("#{meth}_will_change!") unless @attributes[meth] == value
135
+ @attributes[meth] = value
136
+ end
141
137
 
142
- def lazzy_load_field(field_name, *arguments)
143
- if attributes["id"]
144
- load(rpc_execute('read', [id], [field_name], *arguments || object_session)[0] || {})
145
- method_missing(field_name, *arguments)
138
+ def get_association(meth, *args)
139
+ return @associations[meth] || :undef if @skip
140
+ if @loaded_associations.has_key?(meth)
141
+ @loaded_associations[meth]
142
+ elsif @associations.has_key?(meth)
143
+ @loaded_associations[meth] = relationnal_result(meth, *args)
144
+ else
145
+ if @attributes["id"]
146
+ @associations[meth] = rpc_execute('read', [@attributes["id"]], [meth], *args || object_session)[0][meth]
147
+ @loaded_associations[meth] = relationnal_result(meth, *args)
148
+ elsif self.class.one2many_associations.has_key?(meth) || self.class.many2many_associations.has_key?(meth)
149
+ load_x2m_association(self.class.all_fields[meth]['relation'], [], *args)
146
150
  else
147
151
  nil
148
152
  end
149
153
  end
154
+ end
155
+
156
+ def set_association(meth, *args)
157
+ value = sanitize_association(meth, args[0])
158
+ if self.class.many2one_associations.has_key?(meth) # TODO detect false positives changes for other associations too
159
+ if @associations[meth].is_a?(Array) && @associations[meth][0] == value \
160
+ || @associations[meth] == value #\
161
+ return value
162
+ end
163
+ end
164
+ @skip = true
165
+ send("#{meth}_will_change!")
166
+ @skip = false
167
+ if value.is_a?(Ooor::Base) || value.is_a?(Array) && !value.empty? && value.all? {|i| i.is_a?(Ooor::Base)}
168
+ @loaded_associations[meth] = value
169
+ else
170
+ @loaded_associations.delete(meth)
171
+ end
172
+ @associations[meth] = value
173
+ end
174
+
175
+ # # Raise NoMethodError if the named attribute does not exist in order to preserve behavior expected by #clone.
176
+ # def attribute(name)
177
+ # key = name.to_s
178
+ # if self.class.fields.has_key?(key) #TODO check not symbols
179
+ # get_attribute(key)
180
+ # elsif self.class.associations_keys.index(key)
181
+ # get_association(key)
182
+ # else
183
+ # raise NoMethodError
184
+ # end
185
+ # end
186
+
187
+ def method_missing(method_symbol, *arguments)
188
+ self.class.reload_fields_definition(false, object_session)
189
+ if id
190
+ rpc_execute(method_symbol, [id], *arguments) #we assume that's an action
191
+ else
192
+ super
193
+ end
194
+ rescue UnknownAttributeOrAssociationError => e
195
+ e.klass = self.class
196
+ raise e
197
+ end
150
198
 
151
199
  end
152
200
  end