sugarcrm 0.9.10 → 0.9.11
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.rdoc +38 -6
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/sugarcrm +26 -0
- data/lib/sugarcrm.rb +2 -2
- data/lib/sugarcrm/associations/association.rb +11 -8
- data/lib/sugarcrm/associations/association_collection.rb +1 -1
- data/lib/sugarcrm/associations/association_methods.rb +1 -1
- data/lib/sugarcrm/attributes/attribute_methods.rb +7 -2
- data/lib/sugarcrm/attributes/attribute_typecast.rb +2 -2
- data/lib/sugarcrm/base.rb +37 -236
- data/lib/sugarcrm/connection/api/get_available_modules.rb +2 -2
- data/lib/sugarcrm/connection/api/get_document_revision.rb +2 -2
- data/lib/sugarcrm/connection/api/get_entries.rb +2 -2
- data/lib/sugarcrm/connection/api/get_entries_count.rb +1 -1
- data/lib/sugarcrm/connection/api/get_entry.rb +2 -2
- data/lib/sugarcrm/connection/api/get_entry_list.rb +2 -2
- data/lib/sugarcrm/connection/api/get_module_fields.rb +2 -2
- data/lib/sugarcrm/connection/api/get_note_attachment.rb +1 -1
- data/lib/sugarcrm/connection/api/get_relationships.rb +2 -2
- data/lib/sugarcrm/connection/api/get_report_entries.rb +1 -1
- data/lib/sugarcrm/connection/api/get_server_info.rb +1 -1
- data/lib/sugarcrm/connection/api/get_user_id.rb +1 -1
- data/lib/sugarcrm/connection/api/get_user_team_id.rb +1 -1
- data/lib/sugarcrm/connection/api/logout.rb +1 -1
- data/lib/sugarcrm/connection/api/seamless_login.rb +1 -1
- data/lib/sugarcrm/connection/api/search_by_module.rb +1 -1
- data/lib/sugarcrm/connection/api/set_campaign_merge.rb +1 -1
- data/lib/sugarcrm/connection/api/set_document_revision.rb +1 -1
- data/lib/sugarcrm/connection/api/set_entries.rb +1 -1
- data/lib/sugarcrm/connection/api/set_entry.rb +1 -1
- data/lib/sugarcrm/connection/api/set_note_attachment.rb +1 -1
- data/lib/sugarcrm/connection/api/set_relationship.rb +1 -1
- data/lib/sugarcrm/connection/api/set_relationships.rb +1 -1
- data/lib/sugarcrm/connection/connection.rb +5 -10
- data/lib/sugarcrm/connection/helper.rb +3 -2
- data/lib/sugarcrm/connection/response.rb +8 -6
- data/lib/sugarcrm/exceptions.rb +3 -0
- data/lib/sugarcrm/finders.rb +2 -0
- data/lib/sugarcrm/{dynamic_finder_match.rb → finders/dynamic_finder_match.rb} +0 -0
- data/lib/sugarcrm/finders/finder_methods.rb +236 -0
- data/lib/sugarcrm/module.rb +35 -11
- data/lib/sugarcrm/module_methods.rb +68 -23
- data/lib/sugarcrm/session.rb +179 -0
- data/test/connection/test_get_available_modules.rb +1 -4
- data/test/connection/test_get_entries.rb +2 -8
- data/test/connection/test_get_entry.rb +1 -2
- data/test/connection/test_get_entry_list.rb +6 -12
- data/test/connection/test_get_module_fields.rb +1 -4
- data/test/connection/test_get_relationships.rb +1 -4
- data/test/connection/test_get_server_info.rb +1 -4
- data/test/connection/test_get_user_id.rb +1 -4
- data/test/connection/test_get_user_team_id.rb +1 -4
- data/test/connection/test_login.rb +3 -5
- data/test/connection/test_logout.rb +1 -4
- data/test/connection/test_set_note_attachment.rb +1 -2
- data/test/connection/test_set_relationship.rb +1 -2
- data/test/helper.rb +6 -8
- data/test/test_association_collection.rb +1 -2
- data/test/test_associations.rb +18 -1
- data/test/test_connection.rb +2 -6
- data/test/test_module.rb +34 -6
- data/test/test_response.rb +2 -3
- data/test/test_session.rb +109 -0
- data/test/test_sugarcrm.rb +58 -7
- metadata +14 -11
- data/lib/sugarcrm/environment.rb +0 -63
- data/test/test_environment.rb +0 -45
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -28,6 +28,9 @@ A less clunky way to interact with SugarCRM via REST.
|
|
28
28
|
# Enable debugging on the current connection
|
29
29
|
SugarCRM.connection.debug = true
|
30
30
|
|
31
|
+
# Reload the environment (will make the gem classes reflect changes made on SugarCRM server, such as adding fields)
|
32
|
+
SugarCRM.reload!
|
33
|
+
|
31
34
|
# Get the logged in user
|
32
35
|
SugarCRM.current_user
|
33
36
|
|
@@ -61,7 +64,7 @@ A less clunky way to interact with SugarCRM via REST.
|
|
61
64
|
# Retrieve all Email Addresses on an Account
|
62
65
|
SugarCRM::Account.find_by_name("JAB Funds Ltd.").contacts.each do |contact|
|
63
66
|
contact.email_addresses.each do |email|
|
64
|
-
puts "#{email.email_address}" unless email.opt_out
|
67
|
+
puts "#{email.email_address}" unless email.opt_out
|
65
68
|
end
|
66
69
|
end
|
67
70
|
|
@@ -145,7 +148,7 @@ If you want to use a configuration file instead of always specifying the url, us
|
|
145
148
|
* `~/.sugarcrm.yaml` (i.e. your home directory on Linux and Mac OSX)
|
146
149
|
* a `sugarcrm.yaml` file at the root of you Windows home directory (execute `ENV['USERPROFILE']` in Ruby to see which directory should contain the file)
|
147
150
|
* `config/sugarcrm.yaml` (will need to be copied each time you upgrade or reinstall the gem)
|
148
|
-
* a YAML file and call `SugarCRM
|
151
|
+
* a YAML file and call `SugarCRM.load_config` followed by the absolute path to your configuration file
|
149
152
|
|
150
153
|
If there are several configuration files, they are loaded sequentially in the order above and will overwrite previous values (if present). This allows you to (e.g.) have a config file in `/etc/sugarcrm.yaml` with system-wide configuration information (such as the url where SugarCRM is located) and/or defaults. Each developer/user can then have his personal configuration file in `~/.sugarcrm.yaml` with his own username and password. A developer could also specify a different location for the SugarCRM instance (e.g. a local testing instance) in his configuration file, which will take precedence over the value in `/etc/sugarcrm.yaml`.
|
151
154
|
|
@@ -165,10 +168,12 @@ An example, accompanied by instructions, can be found in the `config/sugarcrm.ya
|
|
165
168
|
2. Require the gem with `require 'sugarcrm'`
|
166
169
|
|
167
170
|
3. * if your login credentials are stored in the `config/sugarcrm.yaml` file, you have been automagically logged in already ;
|
168
|
-
* if your login credentials are stored in a different config file, just call `SugarCRM
|
171
|
+
* if your login credentials are stored in a different config file, just call `SugarCRM.load_config` followed by the absolute path to your config file. This will log you in automatically ;
|
169
172
|
* if you don't have a configuration file, you can still call the basic `SugarCRM.connect` and give it the proper arguments (see documentation above)
|
170
173
|
|
171
174
|
4. You now have full access to the gem's functionality, e.g. `puts SugarCRM::Account.first.name`
|
175
|
+
|
176
|
+
5. If you make changes on the SugarCRM server (e.g. adding a field to a module), you can call `SugarCRM.reload!` to rebuild the gem's modules and gain access to the new fields
|
172
177
|
|
173
178
|
== EXTENDING THE GEM
|
174
179
|
|
@@ -176,11 +181,38 @@ If you want to extend the gem's capabilities (e.g. to add methods specific to yo
|
|
176
181
|
|
177
182
|
* drop your `*.rb` files in `lib/sugarcrm/extensions/` (see the README in that folder)
|
178
183
|
|
179
|
-
* drop your `*.rb` files in any other folder and call `SugarCRM
|
184
|
+
* drop your `*.rb` files in any other folder and call `SugarCRM.extensions_folder = ` followed by the absolute path to the folder containing your extensions
|
185
|
+
|
186
|
+
== WORKING WITH SIMULTANEOUS SESSIONS
|
187
|
+
|
188
|
+
This gem allows you to work with several SugarCRM session simultaneously: on each `SugarCRM.connect` call, a namespace is returned. Make sure you do NOT store this namespace in a reserved name (such as SugarCRM). This namespace can then be used just like you would use the `SugarCRM` module. For example:
|
189
|
+
|
190
|
+
ServerOne = SugarCRM.connect(URL1,...)
|
191
|
+
ServerOne::User.first
|
192
|
+
ServerTwo = SugarCRM.connect(URL2,...)
|
193
|
+
ServerTwo::User.first
|
194
|
+
|
195
|
+
If you have only one active session, calls to SugarCRM are delegated to the active session's namespace, like so
|
196
|
+
|
197
|
+
ServerOne = SugarCRM.connect(...)
|
198
|
+
ServerOne::User.first # this call does
|
199
|
+
SugarCRM::User.first # the exact same thing as this one
|
200
|
+
|
201
|
+
To replace your session to connect with different credentials, use
|
202
|
+
|
203
|
+
ServerOne.reconnect(...)
|
204
|
+
|
205
|
+
Then your session will be reused (SugarCRM modules will be reloaded).
|
206
|
+
|
207
|
+
To disconnect an active session:
|
208
|
+
|
209
|
+
ServerOne.disconnect!
|
180
210
|
|
181
211
|
== REQUIREMENTS:
|
182
212
|
|
183
|
-
* >=
|
213
|
+
* activesupport >= 3.0.0
|
214
|
+
* i18n
|
215
|
+
* json
|
184
216
|
|
185
217
|
== INSTALL:
|
186
218
|
|
@@ -188,7 +220,7 @@ If you want to extend the gem's capabilities (e.g. to add methods specific to yo
|
|
188
220
|
|
189
221
|
== TEST:
|
190
222
|
|
191
|
-
|
223
|
+
Put your credentials in a file called `test/config.yaml` (which you will have to create). These must point to a SugarCRM test instance with demo data. See an example file in `test/config_test.yaml` (leave that file as is).
|
192
224
|
|
193
225
|
== Note on Patches/Pull Requests
|
194
226
|
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.9.
|
1
|
+
0.9.11
|
data/bin/sugarcrm
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'irb'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'sugarcrm'
|
7
|
+
rescue LoadError
|
8
|
+
sugarcrm_path = File.join(File.dirname(__FILE__), '..', 'lib')
|
9
|
+
$:.unshift(sugarcrm_path)
|
10
|
+
require 'sugarcrm'
|
11
|
+
end
|
12
|
+
puts <<-EOF
|
13
|
+
Welcome to the SugarCRM Console!
|
14
|
+
EOF
|
15
|
+
|
16
|
+
IRB.start
|
17
|
+
IRB.conf[:PROMPT][:SUGARCRM] = {
|
18
|
+
:PROMPT_C => "SugarCRM :%03n > ",
|
19
|
+
:AUTO_INDENT=>true,
|
20
|
+
:RETURN=>" => %s \n",
|
21
|
+
:PROMPT_I=>"SugarCRM :%03n > ",
|
22
|
+
:PROMPT_N=>"SugarCRM :%03n?> ",
|
23
|
+
:PROMPT_S=>"SugarCRM :%03n%l> "
|
24
|
+
}
|
25
|
+
IRB.conf[:PROMPT_MODE] = :SUGARCRM
|
26
|
+
|
data/lib/sugarcrm.rb
CHANGED
@@ -8,13 +8,13 @@ require 'rubygems'
|
|
8
8
|
require 'active_support/core_ext'
|
9
9
|
require 'json'
|
10
10
|
|
11
|
-
require 'sugarcrm/
|
11
|
+
require 'sugarcrm/session'
|
12
12
|
require 'sugarcrm/module_methods'
|
13
13
|
require 'sugarcrm/connection'
|
14
14
|
require 'sugarcrm/exceptions'
|
15
|
+
require 'sugarcrm/finders'
|
15
16
|
require 'sugarcrm/attributes'
|
16
17
|
require 'sugarcrm/associations'
|
17
|
-
require 'sugarcrm/dynamic_finder_match'
|
18
18
|
require 'sugarcrm/module'
|
19
19
|
require 'sugarcrm/base'
|
20
20
|
|
@@ -53,9 +53,13 @@ module SugarCRM
|
|
53
53
|
end
|
54
54
|
|
55
55
|
def to_s
|
56
|
-
"
|
56
|
+
"#<#{@owner.class.session.namespace_const}::Association @proxy_methods=[#{@proxy_methods.join(", ")}], " +
|
57
57
|
"@link_field=\"#{@link_field}\", @target=#{@target}, @owner=#{@owner.class}, " +
|
58
|
-
"@cardinality
|
58
|
+
"@cardinality=:#{@cardinality}>"
|
59
|
+
end
|
60
|
+
|
61
|
+
def pretty_print(pp)
|
62
|
+
pp.text self.inspect, 0
|
59
63
|
end
|
60
64
|
|
61
65
|
protected
|
@@ -69,16 +73,17 @@ module SugarCRM
|
|
69
73
|
def resolve_target
|
70
74
|
# Use the link_field name first
|
71
75
|
klass = @link_field.singularize.camelize
|
72
|
-
|
76
|
+
namespace = @owner.class.session.namespace_const
|
77
|
+
return namespace.const_get(klass) if namespace.const_defined? klass
|
73
78
|
# Use the link_field attribute "module"
|
74
79
|
if @attributes["module"].length > 0
|
75
|
-
module_name = SugarCRM::Module.find(@attributes["module"])
|
76
|
-
return
|
80
|
+
module_name = SugarCRM::Module.find(@attributes["module"], @owner.class.session)
|
81
|
+
return namespace.const_get(module_name.klass) if namespace.const_defined? module_name.klass
|
77
82
|
end
|
78
83
|
# Use the "relationship" target
|
79
84
|
if @attributes["relationship"].length > 0
|
80
85
|
klass = @relationship[:target][:name].singularize.camelize
|
81
|
-
return
|
86
|
+
return namespace.const_get(klass) if namespace.const_defined? klass
|
82
87
|
end
|
83
88
|
false
|
84
89
|
end
|
@@ -146,11 +151,9 @@ module SugarCRM
|
|
146
151
|
}
|
147
152
|
end
|
148
153
|
|
149
|
-
# TODO: Add Tests for This
|
150
154
|
def resolve_cardinality
|
151
155
|
"#{@relationship[:owner][:cardinality]}_to_#{@relationship[:target][:cardinality]}".to_sym
|
152
156
|
end
|
153
|
-
|
154
157
|
end
|
155
158
|
end
|
156
159
|
|
@@ -106,7 +106,7 @@ module SugarCRM
|
|
106
106
|
|
107
107
|
# Loads related records for the given association
|
108
108
|
def load_associated_records
|
109
|
-
array =
|
109
|
+
array = @owner.class.session.connection.get_relationships(@owner.class._module.name, @owner.id, @association.to_s)
|
110
110
|
@loaded = true
|
111
111
|
# we use original to track the state of the collection at start
|
112
112
|
@collection = Array.wrap(array).dup
|
@@ -29,7 +29,7 @@ module SugarCRM; module AssociationMethods
|
|
29
29
|
targets = Array.wrap(target)
|
30
30
|
targets.each do |t|
|
31
31
|
association = @associations.find!(t)
|
32
|
-
response =
|
32
|
+
response = self.class.session.connection.set_relationship(
|
33
33
|
self.class._module.name, self.id,
|
34
34
|
association.link_field, [t.id], opts
|
35
35
|
)
|
@@ -32,7 +32,12 @@ module SugarCRM; module AttributeMethods
|
|
32
32
|
# Default to = if we can't resolve the condition.
|
33
33
|
operator ||= '='
|
34
34
|
# Extract value from query
|
35
|
-
value = $3
|
35
|
+
value = $3
|
36
|
+
if [TrueClass, FalseClass].include? attribute_condition.class
|
37
|
+
# fix value for checkboxes: users can pass true/false as condition, should be converted to '1' or '0' respectively
|
38
|
+
value = (attribute_condition.class == TrueClass ? '1' : '0')
|
39
|
+
end
|
40
|
+
|
36
41
|
# TODO: Write a test for sending invalid attribute names.
|
37
42
|
# strip single quotes
|
38
43
|
value = value.strip[/'?([^']*)'?/,1]
|
@@ -129,7 +134,7 @@ module SugarCRM; module AttributeMethods
|
|
129
134
|
# Complain if we aren't valid
|
130
135
|
raise InvalidRecord, errors.to_a.join(", ") if !valid?
|
131
136
|
# Send the save request
|
132
|
-
response =
|
137
|
+
response = self.class.session.connection.set_entry(self.class._module.name, serialize_modified_attributes)
|
133
138
|
# Complain if we don't get a parseable response back
|
134
139
|
raise RecordsaveFailed, "Failed to save record: #{self}. Response was not a Hash" unless response.is_a? Hash
|
135
140
|
# Complain if we don't get a valid id back
|
@@ -6,8 +6,8 @@ module SugarCRM; module AttributeTypeCast
|
|
6
6
|
def attr_type_for(attribute)
|
7
7
|
fields = self.class._module.fields
|
8
8
|
field = fields[attribute]
|
9
|
-
raise UninitializedModule, "
|
10
|
-
raise InvalidAttribute, "#{self.class}_module.fields does not contain an entry for #{attribute} (of type: #{attribute.class})\nValid fields: #{self.class._module.fields.keys.sort.join(", ")}" if field.nil?
|
9
|
+
raise UninitializedModule, "#{self.class.session.namespace_const}Module #{self.class._module.name} was not initialized properly (fields.length == 0)" if fields.length == 0
|
10
|
+
raise InvalidAttribute, "#{self.class}._module.fields does not contain an entry for #{attribute} (of type: #{attribute.class})\nValid fields: #{self.class._module.fields.keys.sort.join(", ")}" if field.nil?
|
11
11
|
raise InvalidAttributeType, "#{self.class}._module.fields[#{attribute}] does not have a key for \'type\'" if field["type"].nil?
|
12
12
|
field["type"].to_sym
|
13
13
|
end
|
data/lib/sugarcrm/base.rb
CHANGED
@@ -3,9 +3,6 @@ module SugarCRM; class Base
|
|
3
3
|
# Unset all of the instance methods we don't need.
|
4
4
|
instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$|^define_method$|^class$|^nil.$|^methods$|^instance_of.$|^respond_to)/ }
|
5
5
|
|
6
|
-
# This holds our connection
|
7
|
-
cattr_accessor :connection, :instance_writer => false
|
8
|
-
|
9
6
|
# Tracks if we have extended our class with attribute methods yet.
|
10
7
|
class_attribute :attribute_methods_generated
|
11
8
|
self.attribute_methods_generated = false
|
@@ -16,6 +13,10 @@ module SugarCRM; class Base
|
|
16
13
|
class_attribute :_module
|
17
14
|
self._module = nil
|
18
15
|
|
16
|
+
# the session to which we're linked
|
17
|
+
class_attribute :session
|
18
|
+
self.session = nil
|
19
|
+
|
19
20
|
# Contains a list of attributes
|
20
21
|
attr :attributes, true
|
21
22
|
attr :modified_attributes, true
|
@@ -24,28 +25,32 @@ module SugarCRM; class Base
|
|
24
25
|
attr :errors, true
|
25
26
|
|
26
27
|
class << self # Class methods
|
27
|
-
def establish_connection(url, user, pass, opts={})
|
28
|
-
options = {
|
29
|
-
:debug => false,
|
30
|
-
:register_modules => true
|
31
|
-
}.merge(opts)
|
32
|
-
@debug = options[:debug]
|
33
|
-
@@connection = SugarCRM::Connection.new(url, user, pass, options)
|
34
|
-
end
|
35
|
-
|
36
28
|
def find(*args)
|
37
29
|
options = args.extract_options!
|
30
|
+
options = {:order_by => 'date_entered'}.merge(options)
|
38
31
|
validate_find_options(options)
|
39
32
|
|
40
33
|
case args.first
|
41
34
|
when :first
|
42
35
|
find_initial(options)
|
36
|
+
when :last
|
37
|
+
begin
|
38
|
+
options[:order_by] = reverse_order_clause(options[:order_by])
|
39
|
+
rescue Exception => e
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
find_initial(options)
|
43
43
|
when :all
|
44
44
|
Array.wrap(find_every(options)).compact
|
45
45
|
else
|
46
46
|
find_from_ids(args, options)
|
47
47
|
end
|
48
48
|
end
|
49
|
+
|
50
|
+
# return the connection to the correct SugarCRM server (there can be several)
|
51
|
+
def connection
|
52
|
+
self.parent.session.connection
|
53
|
+
end
|
49
54
|
|
50
55
|
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
|
51
56
|
# same arguments to this method as you can to <tt>find(:first)</tt>.
|
@@ -53,13 +58,19 @@ module SugarCRM; class Base
|
|
53
58
|
find(:first, *args)
|
54
59
|
end
|
55
60
|
|
61
|
+
# A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
|
62
|
+
# same arguments to this method as you can to <tt>find(:last)</tt>.
|
63
|
+
def last(*args)
|
64
|
+
find(:last, *args)
|
65
|
+
end
|
66
|
+
|
56
67
|
# This is an alias for find(:all). You can pass in all the same arguments to this method as you can
|
57
68
|
# to find(:all)
|
58
69
|
def all(*args)
|
59
70
|
find(:all, *args)
|
60
71
|
end
|
61
72
|
|
62
|
-
# Creates an object (or multiple objects) and saves it to SugarCRM
|
73
|
+
# Creates an object (or multiple objects) and saves it to SugarCRM if validations pass.
|
63
74
|
# The resulting object is returned whether the object was saved successfully to the database or not.
|
64
75
|
#
|
65
76
|
# The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
|
@@ -92,226 +103,6 @@ module SugarCRM; class Base
|
|
92
103
|
end
|
93
104
|
end
|
94
105
|
|
95
|
-
private
|
96
|
-
|
97
|
-
def find_initial(options)
|
98
|
-
options.update(:limit => 1)
|
99
|
-
result = find_by_sql(options)
|
100
|
-
return result.first if result.instance_of? Array # find_by_sql will return an Array if result are found
|
101
|
-
result
|
102
|
-
end
|
103
|
-
|
104
|
-
def find_from_ids(ids, options)
|
105
|
-
expects_array = ids.first.kind_of?(Array)
|
106
|
-
return ids.first if expects_array && ids.first.empty?
|
107
|
-
|
108
|
-
ids = ids.flatten.compact.uniq
|
109
|
-
|
110
|
-
case ids.size
|
111
|
-
when 0
|
112
|
-
raise RecordNotFound, "Couldn't find #{self._module.name} without an ID"
|
113
|
-
when 1
|
114
|
-
result = find_one(ids.first, options)
|
115
|
-
expects_array ? [ result ] : result
|
116
|
-
else
|
117
|
-
find_some(ids, options)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def find_one(id, options)
|
122
|
-
if result = SugarCRM.connection.get_entry(self._module.name, id, {:fields => self._module.fields.keys})
|
123
|
-
result
|
124
|
-
else
|
125
|
-
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def find_some(ids, options)
|
130
|
-
result = SugarCRM.connection.get_entries(self._module.name, ids, {:fields => self._module.fields.keys})
|
131
|
-
|
132
|
-
# Determine expected size from limit and offset, not just ids.size.
|
133
|
-
expected_size =
|
134
|
-
if options[:limit] && ids.size > options[:limit]
|
135
|
-
options[:limit]
|
136
|
-
else
|
137
|
-
ids.size
|
138
|
-
end
|
139
|
-
|
140
|
-
# 11 ids with limit 3, offset 9 should give 2 results.
|
141
|
-
if options[:offset] && (ids.size - options[:offset] < expected_size)
|
142
|
-
expected_size = ids.size - options[:offset]
|
143
|
-
end
|
144
|
-
|
145
|
-
if result.size == expected_size
|
146
|
-
result
|
147
|
-
else
|
148
|
-
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def find_every(options)
|
153
|
-
find_by_sql(options)
|
154
|
-
end
|
155
|
-
|
156
|
-
def find_by_sql(options)
|
157
|
-
# SugarCRM REST API has a bug where, when :limit and :offset options are passed simultaneously, :limit is considered to be the smallest of the two, and :offset is the larger
|
158
|
-
# in addition to allowing querying of large datasets while avoiding timeouts,
|
159
|
-
# this implementation fixes the :limit - :offset bug so that it behaves correctly
|
160
|
-
local_options = {}
|
161
|
-
options.keys.each{|k|
|
162
|
-
local_options[k] = options[k]
|
163
|
-
}
|
164
|
-
local_options.delete(:offset) if local_options[:offset] == 0
|
165
|
-
|
166
|
-
# store the number of records wanted by user, as we'll overwrite :limit option to obtain several slices of records (to avoid timeout issues)
|
167
|
-
nb_to_fetch = local_options[:limit]
|
168
|
-
nb_to_fetch = nb_to_fetch.to_i if nb_to_fetch
|
169
|
-
offset_value = local_options[:offset] || 10 # arbitrary value, must be bigger than :limit used (see comment above)
|
170
|
-
offset_value = offset_value.to_i
|
171
|
-
offset_value.freeze
|
172
|
-
initial_limit = nb_to_fetch.nil? ? offset_value : [offset_value, nb_to_fetch].min # how many records should be fetched on first pass
|
173
|
-
# ensure results are ordered so :limit and :offset option behave in a deterministic fashion
|
174
|
-
local_options = { :order_by => :id }.merge(local_options)
|
175
|
-
local_options.update(:limit => initial_limit) # override original argument
|
176
|
-
|
177
|
-
# get first slice of results
|
178
|
-
# note: to work around a SugarCRM REST API bug, the :limit option must always be smaller than the :offset option
|
179
|
-
# this is the reason this first query is separate (not in the loop): the initial query has a larger limit, so that we can then use the loop
|
180
|
-
# with :limit always smaller than :offset
|
181
|
-
results = SugarCRM.connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
|
182
|
-
return nil unless results
|
183
|
-
results = Array.wrap(results)
|
184
|
-
|
185
|
-
limit_value = [5, offset_value].min # arbitrary value, must be smaller than :offset used (see comment above)
|
186
|
-
limit_value.freeze
|
187
|
-
local_options = { :order_by => :id }.merge(local_options)
|
188
|
-
local_options.update(:limit => limit_value)
|
189
|
-
|
190
|
-
# a portion of the results has already been queried
|
191
|
-
# update or set the :offset value to reflect this
|
192
|
-
local_options[:offset] ||= results.size
|
193
|
-
local_options[:offset] += offset_value
|
194
|
-
|
195
|
-
# continue fetching results until we either
|
196
|
-
# a) have as many results as the user wants (specified via the original :limit option)
|
197
|
-
# b) there are no more results matching the criteria
|
198
|
-
while result_slice = SugarCRM.connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
|
199
|
-
results.concat(Array.wrap(result_slice))
|
200
|
-
# make sure we don't return more results than the user requested (via original :limit option)
|
201
|
-
if nb_to_fetch && results.size >= nb_to_fetch
|
202
|
-
return results.slice(0, nb_to_fetch)
|
203
|
-
end
|
204
|
-
local_options[:offset] += local_options[:limit] # update :offset as we get more records
|
205
|
-
end
|
206
|
-
results
|
207
|
-
end
|
208
|
-
|
209
|
-
def query_from_options(options)
|
210
|
-
# If we dont have conditions, just return an empty query
|
211
|
-
return "" unless options[:conditions]
|
212
|
-
conditions = []
|
213
|
-
options[:conditions].each do |condition|
|
214
|
-
# Merge the result into the conditions array
|
215
|
-
conditions |= flatten_conditions_for(condition)
|
216
|
-
end
|
217
|
-
conditions.join(" AND ")
|
218
|
-
end
|
219
|
-
|
220
|
-
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
|
221
|
-
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
|
222
|
-
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
|
223
|
-
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
|
224
|
-
#
|
225
|
-
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
|
226
|
-
# is actually <tt>find_all_by_amount(amount, options)</tt>.
|
227
|
-
#
|
228
|
-
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
|
229
|
-
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
|
230
|
-
# respectively.
|
231
|
-
#
|
232
|
-
# Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
|
233
|
-
# attempts to use it do not run through method_missing.
|
234
|
-
def method_missing(method_id, *arguments, &block)
|
235
|
-
if match = DynamicFinderMatch.match(method_id)
|
236
|
-
attribute_names = match.attribute_names
|
237
|
-
super unless all_attributes_exists?(attribute_names)
|
238
|
-
if match.finder?
|
239
|
-
finder = match.finder
|
240
|
-
bang = match.bang?
|
241
|
-
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
242
|
-
def self.#{method_id}(*args)
|
243
|
-
options = args.extract_options!
|
244
|
-
attributes = construct_attributes_from_arguments(
|
245
|
-
[:#{attribute_names.join(',:')}],
|
246
|
-
args
|
247
|
-
)
|
248
|
-
finder_options = { :conditions => attributes }
|
249
|
-
validate_find_options(options)
|
250
|
-
|
251
|
-
#{'result = ' if bang}if options[:conditions]
|
252
|
-
with_scope(:find => finder_options) do
|
253
|
-
find(:#{finder}, options)
|
254
|
-
end
|
255
|
-
else
|
256
|
-
find(:#{finder}, options.merge(finder_options))
|
257
|
-
end
|
258
|
-
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
259
|
-
end
|
260
|
-
EOS
|
261
|
-
send(method_id, *arguments)
|
262
|
-
elsif match.instantiator?
|
263
|
-
instantiator = match.instantiator
|
264
|
-
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
265
|
-
def self.#{method_id}(*args)
|
266
|
-
attributes = [:#{attribute_names.join(',:')}]
|
267
|
-
protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
|
268
|
-
args.each_with_index do |arg, i|
|
269
|
-
if arg.is_a?(Hash)
|
270
|
-
protected_attributes_for_create = args[i].with_indifferent_access
|
271
|
-
else
|
272
|
-
unprotected_attributes_for_create[attributes[i]] = args[i]
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
|
-
find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
|
277
|
-
|
278
|
-
options = { :conditions => find_attributes }
|
279
|
-
|
280
|
-
record = find(:first, options)
|
281
|
-
|
282
|
-
if record.nil?
|
283
|
-
record = self.new(unprotected_attributes_for_create)
|
284
|
-
#{'record.save' if instantiator == :create}
|
285
|
-
record
|
286
|
-
else
|
287
|
-
record
|
288
|
-
end
|
289
|
-
end
|
290
|
-
EOS
|
291
|
-
send(method_id, *arguments, &block)
|
292
|
-
end
|
293
|
-
else
|
294
|
-
super
|
295
|
-
end
|
296
|
-
end
|
297
|
-
|
298
|
-
def all_attributes_exists?(attribute_names)
|
299
|
-
attribute_names.all? { |name| attributes_from_module.include?(name) }
|
300
|
-
end
|
301
|
-
|
302
|
-
def construct_attributes_from_arguments(attribute_names, arguments)
|
303
|
-
attributes = {}
|
304
|
-
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
|
305
|
-
attributes
|
306
|
-
end
|
307
|
-
|
308
|
-
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
|
309
|
-
:order_by, :select, :readonly, :group, :having, :from, :lock ]
|
310
|
-
|
311
|
-
def validate_find_options(options) #:nodoc:
|
312
|
-
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
313
|
-
end
|
314
|
-
|
315
106
|
end
|
316
107
|
|
317
108
|
# Creates an instance of a Module Class, i.e. Account, User, Contact, etc.
|
@@ -363,7 +154,12 @@ module SugarCRM; class Base
|
|
363
154
|
params = {}
|
364
155
|
params[:id] = serialize_id
|
365
156
|
params[:deleted]= {:name => "deleted", :value => "1"}
|
366
|
-
(
|
157
|
+
(self.class.connection.set_entry(self.class._module.name, params).class == Hash)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Reloads the record from SugarCRM
|
161
|
+
def reload!
|
162
|
+
self.attributes = self.class.find(self.id).attributes
|
367
163
|
end
|
368
164
|
|
369
165
|
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
|
@@ -393,13 +189,17 @@ module SugarCRM; class Base
|
|
393
189
|
end
|
394
190
|
self.save
|
395
191
|
end
|
396
|
-
|
192
|
+
|
397
193
|
# Delegates to id in order to allow two records of the same type and id to work with something like:
|
398
194
|
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
|
399
195
|
def hash
|
400
196
|
id.hash
|
401
197
|
end
|
402
|
-
|
198
|
+
|
199
|
+
def pretty_print(pp)
|
200
|
+
pp.text self.inspect, 0
|
201
|
+
end
|
202
|
+
|
403
203
|
def attribute_methods_generated?
|
404
204
|
self.class.attribute_methods_generated
|
405
205
|
end
|
@@ -409,6 +209,7 @@ module SugarCRM; class Base
|
|
409
209
|
end
|
410
210
|
|
411
211
|
Base.class_eval do
|
212
|
+
extend FinderMethods::ClassMethods
|
412
213
|
include AttributeMethods
|
413
214
|
extend AttributeMethods::ClassMethods
|
414
215
|
include AttributeValidations
|