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 CHANGED
@@ -10,34 +10,39 @@ RubyGem for interacting with SugarCRM via REST.
10
10
 
11
11
  == Description:
12
12
 
13
- I've implemented all of the basic API calls that SugarCRM supports, and am actively building an abstraction layer
14
- on top of the basic API methods. The end result will be ActiveRecord style finders and first class objects. Some
15
- of this functionality is included today.
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 ID, using the SugarCRM::User Proxy object
29
- SugarCRM::User.find(id)
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 for direct API calls
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
- # Lookup a user by name. Find any associated accounts
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.2
1
+ 0.7.7
@@ -1,12 +1,14 @@
1
1
  module SugarCRM; module AttributeMethods
2
2
 
3
- # Returns a hash of the module fields from the
4
- def attributes_from_module_fields
5
- fields = {}
6
- self.class._module.fields.keys.sort.each do |k|
7
- fields[k.to_s] = nil
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
- # Runs a find against the remote service
37
- def find(id)
38
- response = SugarCRM.connection.get_entry(self._module.name, id, {:fields => self._module.fields.keys})
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
 
@@ -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 = [:get_available_modules]
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
@@ -19,4 +19,7 @@ module SugarCRM
19
19
 
20
20
  class AttributeParsingError < RuntimeError
21
21
  end
22
+
23
+ class RecordNotFound < RuntimeError
24
+ end
22
25
  end
@@ -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
@@ -5,6 +5,7 @@ require 'active_support/core_ext'
5
5
  require 'sugarcrm/module_methods'
6
6
  require 'sugarcrm/base'
7
7
  require 'sugarcrm/connection'
8
+ require 'sugarcrm/dynamic_finder_match'
8
9
  require 'sugarcrm/exceptions'
9
10
  require 'sugarcrm/module'
10
11
  require 'sugarcrm/request'
@@ -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]
@@ -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: 7
4
+ hash: 13
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 7
9
- - 2
10
- version: 0.7.2
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-13 00:00:00 -08:00
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