sugarcrm 0.9.10 → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|