sugarcrm 0.7.2 → 0.7.7
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/README.rdoc +18 -13
- data/VERSION +1 -1
- data/lib/sugarcrm/attribute_methods.rb +8 -6
- data/lib/sugarcrm/base.rb +238 -5
- data/lib/sugarcrm/connection.rb +1 -1
- data/lib/sugarcrm/dynamic_finder_match.rb +41 -0
- data/lib/sugarcrm/exceptions.rb +3 -0
- data/lib/sugarcrm/module.rb +2 -0
- data/lib/sugarcrm.rb +1 -0
- data/test/connection/test_get_available_modules.rb +1 -1
- data/test/test_sugarcrm.rb +14 -0
- metadata +5 -4
data/README.rdoc
CHANGED
@@ -10,34 +10,39 @@ RubyGem for interacting with SugarCRM via REST.
|
|
10
10
|
|
11
11
|
== Description:
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
of
|
13
|
+
A less clunky way to interact with SugarCRM via REST.
|
14
|
+
|
15
|
+
I've built an abstraction layer on top of the SugarCRM REST API, instead of +get_entry("Users", "1")+ you can
|
16
|
+
call +SugarCRM::User.find(1)+. There is also support for collections à la +SugarCRM::User.find(1).accounts+.
|
17
|
+
ActiveRecord style finders are in place, with limited support for conditions and joins
|
18
|
+
e.g. +SugarCRM::Contacts.find_by_title("VP of Sales")+ will work, but +SugarCRM::Contacts.find_by_title("VP of Sales", {:conditions => {:deleted => 0}})+ will not.
|
16
19
|
|
17
20
|
== FEATURES/PROBLEMS:
|
18
21
|
|
22
|
+
* Supports all v2 API calls
|
19
23
|
* Auto-generation of Module specific objects. When a connection is established, get_available_modules is called and the resultant modules are turned into SugarCRM::Module classes.
|
20
|
-
* If you just want to use the vanilla API, you can access the methods directly on the connection object.
|
24
|
+
* If you just want to use the vanilla API, you can access the methods directly on the SugarCRM.connection object.
|
21
25
|
|
22
26
|
== SYNOPSIS:
|
23
27
|
|
24
28
|
require 'sugarcrm'
|
29
|
+
|
25
30
|
# Establish a connection
|
26
31
|
SugarCRM::Base.establish_connection("http://localhost/sugarcrm", 'user', 'password', {:debug => false})
|
27
32
|
|
28
|
-
# Retrieve a user by
|
29
|
-
SugarCRM::User.
|
30
|
-
|
33
|
+
# Retrieve a user by user_name
|
34
|
+
SugarCRM::User.find_by_user_name("admin")
|
35
|
+
|
36
|
+
# Retrieve all Accounts owned by a particular user.
|
37
|
+
SugarCRM::User.find_by_user_name('sarah').accounts
|
38
|
+
|
31
39
|
# Show a list of available modules
|
32
40
|
SugarCRM.modules
|
33
41
|
|
34
|
-
# Use the HTTP
|
35
|
-
SugarCRM.connection.get_entry(1)
|
42
|
+
# Use the HTTP Connection and SugarCRM API to load the Admin user
|
43
|
+
SugarCRM.connection.get_entry("Users", 1)
|
36
44
|
|
37
|
-
#
|
38
|
-
SugarCRM::User.find_by_username('sarah').accounts
|
39
|
-
|
40
|
-
# Same operation, but using the direct API calls
|
45
|
+
# Retrieve all Accounts by user name (direct API method)
|
41
46
|
SugarCRM.connection.get_entry_list(
|
42
47
|
"Users",
|
43
48
|
"users.user_name = \'sarah\'",
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.7
|
@@ -1,12 +1,14 @@
|
|
1
1
|
module SugarCRM; module AttributeMethods
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
fields
|
3
|
+
module ClassMethods
|
4
|
+
# Returns a hash of the module fields from the module
|
5
|
+
def attributes_from_module_fields
|
6
|
+
fields = {}
|
7
|
+
self._module.fields.keys.sort.each do |k|
|
8
|
+
fields[k.to_s] = nil
|
9
|
+
end
|
10
|
+
fields
|
8
11
|
end
|
9
|
-
fields
|
10
12
|
end
|
11
13
|
|
12
14
|
# Generates get/set methods for keys in the attributes hash
|
data/lib/sugarcrm/base.rb
CHANGED
@@ -33,10 +33,242 @@ module SugarCRM; class Base
|
|
33
33
|
@@connection = SugarCRM::Connection.new(url, user, pass, @debug)
|
34
34
|
end
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
36
|
+
def find(*args)
|
37
|
+
options = args.extract_options!
|
38
|
+
validate_find_options(options)
|
39
|
+
|
40
|
+
case args.first
|
41
|
+
when :first then find_initial(options)
|
42
|
+
when :all then find_every(options)
|
43
|
+
else find_from_ids(args, options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
|
48
|
+
# same arguments to this method as you can to <tt>find(:first)</tt>.
|
49
|
+
def first(*args)
|
50
|
+
find(:first, *args)
|
51
|
+
end
|
52
|
+
|
53
|
+
# This is an alias for find(:all). You can pass in all the same arguments to this method as you can
|
54
|
+
# to find(:all)
|
55
|
+
def all(*args)
|
56
|
+
find(:all, *args)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def find_initial(options)
|
62
|
+
options.update(:max_results => 1)
|
63
|
+
find_every(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_from_ids(ids, options)
|
67
|
+
expects_array = ids.first.kind_of?(Array)
|
68
|
+
return ids.first if expects_array && ids.first.empty?
|
69
|
+
|
70
|
+
ids = ids.flatten.compact.uniq
|
71
|
+
|
72
|
+
case ids.size
|
73
|
+
when 0
|
74
|
+
raise RecordNotFound, "Couldn't find #{name} without an ID"
|
75
|
+
when 1
|
76
|
+
result = find_one(ids.first, options)
|
77
|
+
expects_array ? [ result ] : result
|
78
|
+
else
|
79
|
+
find_some(ids, options)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def find_one(id, options)
|
84
|
+
if result = SugarCRM.connection.get_entry(self._module.name, id, {:fields => self._module.fields.keys})
|
85
|
+
result
|
86
|
+
else
|
87
|
+
raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_some(ids, options)
|
92
|
+
result = SugarCRM.connection.get_entries(self._module.name, ids, {:fields => self._module.fields.keys})
|
93
|
+
|
94
|
+
# Determine expected size from limit and offset, not just ids.size.
|
95
|
+
expected_size =
|
96
|
+
if options[:limit] && ids.size > options[:limit]
|
97
|
+
options[:limit]
|
98
|
+
else
|
99
|
+
ids.size
|
100
|
+
end
|
101
|
+
|
102
|
+
# 11 ids with limit 3, offset 9 should give 2 results.
|
103
|
+
if options[:offset] && (ids.size - options[:offset] < expected_size)
|
104
|
+
expected_size = ids.size - options[:offset]
|
105
|
+
end
|
106
|
+
|
107
|
+
if result.size == expected_size
|
108
|
+
result
|
109
|
+
else
|
110
|
+
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def find_every(options)
|
115
|
+
find_by_sql(options)
|
39
116
|
end
|
117
|
+
|
118
|
+
def find_by_sql(options)
|
119
|
+
query = query_from_options(options)
|
120
|
+
SugarCRM.connection.get_entry_list(self._module.name, query, options)
|
121
|
+
end
|
122
|
+
|
123
|
+
def query_from_options(options)
|
124
|
+
conditions = []
|
125
|
+
options[:conditions].each_pair do |column, value|
|
126
|
+
conditions << "#{self._module.table_name}.#{column} = \'#{value}\'"
|
127
|
+
end
|
128
|
+
conditions.join(" AND ")
|
129
|
+
end
|
130
|
+
|
131
|
+
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
|
132
|
+
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
|
133
|
+
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
|
134
|
+
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
|
135
|
+
#
|
136
|
+
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
|
137
|
+
# is actually <tt>find_all_by_amount(amount, options)</tt>.
|
138
|
+
#
|
139
|
+
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
|
140
|
+
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
|
141
|
+
# respectively.
|
142
|
+
#
|
143
|
+
# Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
|
144
|
+
# attempts to use it do not run through method_missing.
|
145
|
+
def method_missing(method_id, *arguments, &block)
|
146
|
+
if match = DynamicFinderMatch.match(method_id)
|
147
|
+
attribute_names = match.attribute_names
|
148
|
+
super unless all_attributes_exists?(attribute_names)
|
149
|
+
if match.finder?
|
150
|
+
finder = match.finder
|
151
|
+
bang = match.bang?
|
152
|
+
# def self.find_by_login_and_activated(*args)
|
153
|
+
# options = args.extract_options!
|
154
|
+
# attributes = construct_attributes_from_arguments(
|
155
|
+
# [:login,:activated],
|
156
|
+
# args
|
157
|
+
# )
|
158
|
+
# finder_options = { :conditions => attributes }
|
159
|
+
# validate_find_options(options)
|
160
|
+
#
|
161
|
+
# if options[:conditions]
|
162
|
+
# with_scope(:find => finder_options) do
|
163
|
+
# find(:first, options)
|
164
|
+
# end
|
165
|
+
# else
|
166
|
+
# find(:first, options.merge(finder_options))
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
170
|
+
def self.#{method_id}(*args)
|
171
|
+
options = args.extract_options!
|
172
|
+
attributes = construct_attributes_from_arguments(
|
173
|
+
[:#{attribute_names.join(',:')}],
|
174
|
+
args
|
175
|
+
)
|
176
|
+
finder_options = { :conditions => attributes }
|
177
|
+
validate_find_options(options)
|
178
|
+
|
179
|
+
#{'result = ' if bang}if options[:conditions]
|
180
|
+
with_scope(:find => finder_options) do
|
181
|
+
find(:#{finder}, options)
|
182
|
+
end
|
183
|
+
else
|
184
|
+
find(:#{finder}, options.merge(finder_options))
|
185
|
+
end
|
186
|
+
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
187
|
+
end
|
188
|
+
EOS
|
189
|
+
send(method_id, *arguments)
|
190
|
+
elsif match.instantiator?
|
191
|
+
instantiator = match.instantiator
|
192
|
+
# def self.find_or_create_by_user_id(*args)
|
193
|
+
# guard_protected_attributes = false
|
194
|
+
#
|
195
|
+
# if args[0].is_a?(Hash)
|
196
|
+
# guard_protected_attributes = true
|
197
|
+
# attributes = args[0].with_indifferent_access
|
198
|
+
# find_attributes = attributes.slice(*[:user_id])
|
199
|
+
# else
|
200
|
+
# find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# options = { :conditions => find_attributes }
|
204
|
+
# set_readonly_option!(options)
|
205
|
+
#
|
206
|
+
# record = find(:first, options)
|
207
|
+
#
|
208
|
+
# if record.nil?
|
209
|
+
# record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
|
210
|
+
# yield(record) if block_given?
|
211
|
+
# record.save
|
212
|
+
# record
|
213
|
+
# else
|
214
|
+
# record
|
215
|
+
# end
|
216
|
+
# end
|
217
|
+
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
218
|
+
def self.#{method_id}(*args)
|
219
|
+
attributes = [:#{attribute_names.join(',:')}]
|
220
|
+
protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
|
221
|
+
args.each_with_index do |arg, i|
|
222
|
+
if arg.is_a?(Hash)
|
223
|
+
protected_attributes_for_create = args[i].with_indifferent_access
|
224
|
+
else
|
225
|
+
unprotected_attributes_for_create[attributes[i]] = args[i]
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
|
230
|
+
|
231
|
+
options = { :conditions => find_attributes }
|
232
|
+
|
233
|
+
record = find(:first, options)
|
234
|
+
|
235
|
+
if record.nil?
|
236
|
+
record = self.new do |r|
|
237
|
+
r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
|
238
|
+
r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
|
239
|
+
end
|
240
|
+
#{'yield(record) if block_given?'}
|
241
|
+
#{'record.save' if instantiator == :create}
|
242
|
+
record
|
243
|
+
else
|
244
|
+
record
|
245
|
+
end
|
246
|
+
end
|
247
|
+
EOS
|
248
|
+
send(method_id, *arguments, &block)
|
249
|
+
end
|
250
|
+
else
|
251
|
+
super
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def all_attributes_exists?(attribute_names)
|
256
|
+
attribute_names.all? { |name| attributes_from_module_fields.include?(name) }
|
257
|
+
end
|
258
|
+
|
259
|
+
def construct_attributes_from_arguments(attribute_names, arguments)
|
260
|
+
attributes = {}
|
261
|
+
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
|
262
|
+
attributes
|
263
|
+
end
|
264
|
+
|
265
|
+
VALID_FIND_OPTIONS = [ :conditions, :include, :joins, :limit, :offset,
|
266
|
+
:order, :select, :readonly, :group, :having, :from, :lock ]
|
267
|
+
|
268
|
+
def validate_find_options(options) #:nodoc:
|
269
|
+
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
270
|
+
end
|
271
|
+
|
40
272
|
end
|
41
273
|
|
42
274
|
# Creates an instance of a Module Class, i.e. Account, User, Contact, etc.
|
@@ -46,7 +278,7 @@ module SugarCRM; class Base
|
|
46
278
|
# a call to Module.register_all
|
47
279
|
def initialize(id=nil, attributes={})
|
48
280
|
@id = id
|
49
|
-
@attributes = attributes_from_module_fields.merge(attributes)
|
281
|
+
@attributes = self.class.attributes_from_module_fields.merge(attributes)
|
50
282
|
@associations = associations_from_module_link_fields
|
51
283
|
define_attribute_methods
|
52
284
|
define_association_methods
|
@@ -76,9 +308,10 @@ module SugarCRM; class Base
|
|
76
308
|
def association_methods_generated?
|
77
309
|
self.class.association_methods_generated
|
78
310
|
end
|
79
|
-
|
311
|
+
|
80
312
|
Base.class_eval do
|
81
313
|
include AttributeMethods
|
314
|
+
extend AttributeMethods::ClassMethods
|
82
315
|
include AssociationMethods
|
83
316
|
end
|
84
317
|
|
data/lib/sugarcrm/connection.rb
CHANGED
@@ -11,7 +11,7 @@ Dir["#{File.dirname(__FILE__)}/connection/api/*.rb"].each { |f| load(f) }
|
|
11
11
|
module SugarCRM; class Connection
|
12
12
|
|
13
13
|
URL = "/service/v2/rest.php"
|
14
|
-
DONT_SHOW_DEBUG_FOR = [
|
14
|
+
DONT_SHOW_DEBUG_FOR = []
|
15
15
|
RESPONSE_IS_NOT_JSON = [:get_user_id, :get_user_team_id]
|
16
16
|
|
17
17
|
attr :url, true
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SugarCRM
|
2
|
+
class DynamicFinderMatch
|
3
|
+
def self.match(method)
|
4
|
+
df_match = self.new(method)
|
5
|
+
df_match.finder ? df_match : nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(method)
|
9
|
+
@finder = :first
|
10
|
+
case method.to_s
|
11
|
+
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
12
|
+
@finder = :last if $1 == 'last_by'
|
13
|
+
@finder = :all if $1 == 'all_by'
|
14
|
+
names = $2
|
15
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
16
|
+
@bang = true
|
17
|
+
names = $1
|
18
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
19
|
+
@instantiator = $1 == 'initialize' ? :new : :create
|
20
|
+
names = $2
|
21
|
+
else
|
22
|
+
@finder = nil
|
23
|
+
end
|
24
|
+
@attribute_names = names && names.split('_and_')
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :finder, :attribute_names, :instantiator
|
28
|
+
|
29
|
+
def finder?
|
30
|
+
!@finder.nil? && @instantiator.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def instantiator?
|
34
|
+
@finder == :first && !@instantiator.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def bang?
|
38
|
+
@bang
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/sugarcrm/exceptions.rb
CHANGED
data/lib/sugarcrm/module.rb
CHANGED
@@ -2,6 +2,7 @@ module SugarCRM
|
|
2
2
|
# A class for handling SugarCRM Modules
|
3
3
|
class Module
|
4
4
|
attr :name, false
|
5
|
+
attr :table_name, false
|
5
6
|
attr :klass, false
|
6
7
|
attr :fields, false
|
7
8
|
attr :link_fields, false
|
@@ -12,6 +13,7 @@ module SugarCRM
|
|
12
13
|
def initialize(name)
|
13
14
|
@name = name
|
14
15
|
@klass = name.classify
|
16
|
+
@table_name = name.tableize
|
15
17
|
@fields = {}
|
16
18
|
@link_fields = {}
|
17
19
|
@fields_registered = false
|
data/lib/sugarcrm.rb
CHANGED
@@ -3,7 +3,7 @@ require 'helper'
|
|
3
3
|
class TestGetAvailableModules < Test::Unit::TestCase
|
4
4
|
context "A SugarCRM.connection" do
|
5
5
|
setup do
|
6
|
-
SugarCRM::Connection.new(URL, USER, PASS)
|
6
|
+
SugarCRM::Connection.new(URL, USER, PASS, {:debug => false})
|
7
7
|
end
|
8
8
|
should "return an array of modules when #get_modules" do
|
9
9
|
assert_instance_of SugarCRM::Module, SugarCRM.connection.get_modules[0]
|
data/test/test_sugarcrm.rb
CHANGED
@@ -16,6 +16,10 @@ class TestSugarCRM < Test::Unit::TestCase
|
|
16
16
|
assert SugarCRM::User.connection.logged_in?
|
17
17
|
end
|
18
18
|
|
19
|
+
should "respond to self.attributes_from_modules_fields" do
|
20
|
+
assert_instance_of Hash, SugarCRM::User.attributes_from_module_fields
|
21
|
+
end
|
22
|
+
|
19
23
|
should "return an instance of itself when #new" do
|
20
24
|
assert_instance_of SugarCRM::User, SugarCRM::User.new
|
21
25
|
end
|
@@ -45,6 +49,16 @@ class TestSugarCRM < Test::Unit::TestCase
|
|
45
49
|
assert_equal "sarah@example.com", u.email_addresses.first.email_address
|
46
50
|
end
|
47
51
|
|
52
|
+
should "return an array of records when sent #find([id1, id2, id3])" do
|
53
|
+
users = SugarCRM::User.find(["seed_sarah_id", 1])
|
54
|
+
assert_equal "Administrator", users.last.title
|
55
|
+
end
|
56
|
+
|
57
|
+
should "return an instance of User when sent User#find_by_username" do
|
58
|
+
u = SugarCRM::User.find_by_user_name("sarah")
|
59
|
+
assert_equal "sarah@example.com", u.email_addresses.first.email_address
|
60
|
+
end
|
61
|
+
|
48
62
|
end
|
49
63
|
|
50
64
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sugarcrm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 7
|
9
|
-
-
|
10
|
-
version: 0.7.
|
9
|
+
- 7
|
10
|
+
version: 0.7.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Carl Hicks
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-11-
|
18
|
+
date: 2010-11-14 00:00:00 -08:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -110,6 +110,7 @@ files:
|
|
110
110
|
- lib/sugarcrm/connection/api/set_relationship.rb
|
111
111
|
- lib/sugarcrm/connection/api/set_relationships.rb
|
112
112
|
- lib/sugarcrm/connection/helper.rb
|
113
|
+
- lib/sugarcrm/dynamic_finder_match.rb
|
113
114
|
- lib/sugarcrm/exceptions.rb
|
114
115
|
- lib/sugarcrm/module.rb
|
115
116
|
- lib/sugarcrm/module_methods.rb
|