forked from chicks/sugarcrm
/
base.rb
388 lines (339 loc) · 14 KB
/
base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
require 'sugarcrm/attribute_methods'
require 'sugarcrm/association_methods'
module SugarCRM; class Base
# Unset all of the instance methods we don't need.
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$|^define_method$|^class$|^methods$|^instance_of.$|^respond_to.$)/ }
# This holds our connection
cattr_accessor :connection, :instance_writer => false
# Tracks if we have extended our class with attribute methods yet.
class_attribute :attribute_methods_generated
self.attribute_methods_generated = false
class_attribute :association_methods_generated
self.association_methods_generated = false
class_attribute :_module
self._module = nil
# Contains a list of attributes
attr :attributes, true
attr :modified_attributes, true
attr :associations, true
attr :id, true
attr :debug, true
attr :errors, true
class << self # Class methods
def establish_connection(url, user, pass, opts={})
options = {
:debug => false,
:register_modules => true,
}.merge(opts)
@debug = options[:debug]
@@connection = SugarCRM::Connection.new(url, user, pass, options)
end
def find(*args)
options = args.extract_options!
validate_find_options(options)
case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:first)</tt>.
def first(*args)
find(:first, *args)
end
# This is an alias for find(:all). You can pass in all the same arguments to this method as you can
# to find(:all)
def all(*args)
find(:all, *args)
end
private
def find_initial(options)
# TODO: Look into fixing this to actually work
#options.update(:max_results => 1)
find_every(options)
end
def find_from_ids(ids, options)
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
ids = ids.flatten.compact.uniq
case ids.size
when 0
raise RecordNotFound, "Couldn't find #{self._module.name} without an ID"
when 1
result = find_one(ids.first, options)
expects_array ? [ result ] : result
else
find_some(ids, options)
end
end
def find_one(id, options)
if result = SugarCRM.connection.get_entry(self._module.name, id, {:fields => self._module.fields.keys})
result
else
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
end
end
def find_some(ids, options)
result = SugarCRM.connection.get_entries(self._module.name, ids, {:fields => self._module.fields.keys})
# Determine expected size from limit and offset, not just ids.size.
expected_size =
if options[:limit] && ids.size > options[:limit]
options[:limit]
else
ids.size
end
# 11 ids with limit 3, offset 9 should give 2 results.
if options[:offset] && (ids.size - options[:offset] < expected_size)
expected_size = ids.size - options[:offset]
end
if result.size == expected_size
result
else
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
end
end
def find_every(options)
find_by_sql(options)
end
def find_by_sql(options)
query = query_from_options(options)
SugarCRM.connection.get_entry_list(self._module.name, query, options)
end
def query_from_options(options)
# If we dont have conditions, just return an empty query
return "" unless options[:conditions]
conditions = []
options[:conditions].each_pair do |column, v|
v = [] << v unless v.class == Array
v.each{|value|
# parse operator in cases where (e.g.) :attribute => '>= some_value', fallback to '=' operator as default
operator = value.to_s[/^([<>=]*)(.*)$/,1]
operator = '=' if operator.nil? || operator.strip == ''
value = $2 # strip the operator from value passed to query
value = value.strip[/'?([^']*)'?/,1]
unless column =~ /_c$/ # attribute name ending with _c implies a custom attribute
condition_attribute = "#{self._module.table_name}.#{column}"
else
condition_attribute = column # if setting a condition on a custom attribute (i.e. created by user in Studio), don't add model table name (or query breaks)
end
conditions << "#{condition_attribute} #{operator} \'#{value}\'"
}
end
conditions.join(" AND ")
end
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
#
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
# is actually <tt>find_all_by_amount(amount, options)</tt>.
#
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
# respectively.
#
# Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
# attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments, &block)
if match = DynamicFinderMatch.match(method_id)
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
if match.finder?
finder = match.finder
bang = match.bang?
# def self.find_by_login_and_activated(*args)
# options = args.extract_options!
# attributes = construct_attributes_from_arguments(
# [:login,:activated],
# args
# )
# finder_options = { :conditions => attributes }
# validate_find_options(options)
#
# if options[:conditions]
# with_scope(:find => finder_options) do
# find(:first, options)
# end
# else
# find(:first, options.merge(finder_options))
# end
# end
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.#{method_id}(*args)
options = args.extract_options!
attributes = construct_attributes_from_arguments(
[:#{attribute_names.join(',:')}],
args
)
finder_options = { :conditions => attributes }
validate_find_options(options)
#{'result = ' if bang}if options[:conditions]
with_scope(:find => finder_options) do
find(:#{finder}, options)
end
else
find(:#{finder}, options.merge(finder_options))
end
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
end
EOS
send(method_id, *arguments)
elsif match.instantiator?
instantiator = match.instantiator
# def self.find_or_create_by_user_id(*args)
# guard_protected_attributes = false
#
# if args[0].is_a?(Hash)
# guard_protected_attributes = true
# attributes = args[0].with_indifferent_access
# find_attributes = attributes.slice(*[:user_id])
# else
# find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
# end
#
# options = { :conditions => find_attributes }
# set_readonly_option!(options)
#
# record = find(:first, options)
#
# if record.nil?
# record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
# yield(record) if block_given?
# record.save
# record
# else
# record
# end
# end
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.#{method_id}(*args)
attributes = [:#{attribute_names.join(',:')}]
protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
args.each_with_index do |arg, i|
if arg.is_a?(Hash)
protected_attributes_for_create = args[i].with_indifferent_access
else
unprotected_attributes_for_create[attributes[i]] = args[i]
end
end
find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
options = { :conditions => find_attributes }
record = find(:first, options)
if record.nil?
record = self.new do |r|
r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
end
#{'yield(record) if block_given?'}
#{'record.save' if instantiator == :create}
record
else
record
end
end
EOS
send(method_id, *arguments, &block)
end
else
super
end
end
def all_attributes_exists?(attribute_names)
attribute_names.all? { |name| attributes_from_module_fields.include?(name) }
end
def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
attributes
end
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
:order_by, :select, :readonly, :group, :having, :from, :lock ]
def validate_find_options(options) #:nodoc:
options.assert_valid_keys(VALID_FIND_OPTIONS)
end
end
# Creates an instance of a Module Class, i.e. Account, User, Contact, etc.
# This call depends upon SugarCRM.modules having actual data in it. If you
# are using Base.establish_connection, you should be fine. But if you are
# using the Connection class by itself, you may need to prime the pump with
# a call to Module.register_all
def initialize(id=nil, attributes={})
@id = id
@attributes = self.class.attributes_from_module_fields.merge(attributes)
@modified_attributes = {}
@associations = self.class.associations_from_module_link_fields
@errors = Set.new
define_attribute_methods
define_association_methods
typecast_attributes
self
end
def inspect
self
end
def to_s
attrs = []
@attributes.keys.sort.each do |k|
attrs << "#{k}: #{attribute_for_inspect(k)}"
end
"#<#{self.class} #{attrs.join(", ")}>"
end
# Saves the current object, checks that required fields are present.
# returns true or false
def save
return false unless changed?
return false unless valid?
# If we get a Hash back, return true. Otherwise return false.
(SugarCRM.connection.set_entry(self.class._module.name, serialize_modified_attributes).class == Hash)
end
# Saves the current object, checks that required fields are present.
# raises an exception if a save fails
def save!
raise InvalidRecord, errors.to_a.join(", ") unless valid?
# If we get a Hash back, return true. Otherwise return false.
(SugarCRM.connection.set_entry(self.class._module.name, serialize_modified_attributes).class == Hash)
end
def delete
return false if @id.blank?
params = {}
params[:id] = serialize_id
params[:deleted]= {:name => "deleted", :value => "1"}
(SugarCRM.connection.set_entry(self.class._module.name, params).class == Hash)
end
# add syntactic sugar to link module instances with dynamic add_* instance methods
# e.g. my_account.add_contact(my_contact) or # my_contact.add_case(my_case)
def method_missing(method_id, *arguments, &block)
if match = DynamicRelaterMatch.match(method_id)
self.class.class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{method_id}(*args)
raise "Too many arguments for method '#{method_id}'. You can only link modules one at a time." if args.size > 1
module_to_link = args.first
SugarCRM.connection.set_relationship(
self.class._module.name,
self.id,
'#{match.module_instance.pluralize}',
[module_to_link.id]
)
end
EOS
send(method_id, *arguments)
else
super
end
end
# Wrapper around class attribute
def attribute_methods_generated?
self.class.attribute_methods_generated
end
def association_methods_generated?
self.class.association_methods_generated
end
Base.class_eval do
include AttributeMethods
extend AttributeMethods::ClassMethods
include AssociationMethods
extend AssociationMethods::ClassMethods
end
end; end