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 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