sugarcrm 0.7.2 → 0.7.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|