sugarcrm 0.9.10 → 0.9.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +1 -1
  2. data/README.rdoc +38 -6
  3. data/Rakefile +1 -0
  4. data/VERSION +1 -1
  5. data/bin/sugarcrm +26 -0
  6. data/lib/sugarcrm.rb +2 -2
  7. data/lib/sugarcrm/associations/association.rb +11 -8
  8. data/lib/sugarcrm/associations/association_collection.rb +1 -1
  9. data/lib/sugarcrm/associations/association_methods.rb +1 -1
  10. data/lib/sugarcrm/attributes/attribute_methods.rb +7 -2
  11. data/lib/sugarcrm/attributes/attribute_typecast.rb +2 -2
  12. data/lib/sugarcrm/base.rb +37 -236
  13. data/lib/sugarcrm/connection/api/get_available_modules.rb +2 -2
  14. data/lib/sugarcrm/connection/api/get_document_revision.rb +2 -2
  15. data/lib/sugarcrm/connection/api/get_entries.rb +2 -2
  16. data/lib/sugarcrm/connection/api/get_entries_count.rb +1 -1
  17. data/lib/sugarcrm/connection/api/get_entry.rb +2 -2
  18. data/lib/sugarcrm/connection/api/get_entry_list.rb +2 -2
  19. data/lib/sugarcrm/connection/api/get_module_fields.rb +2 -2
  20. data/lib/sugarcrm/connection/api/get_note_attachment.rb +1 -1
  21. data/lib/sugarcrm/connection/api/get_relationships.rb +2 -2
  22. data/lib/sugarcrm/connection/api/get_report_entries.rb +1 -1
  23. data/lib/sugarcrm/connection/api/get_server_info.rb +1 -1
  24. data/lib/sugarcrm/connection/api/get_user_id.rb +1 -1
  25. data/lib/sugarcrm/connection/api/get_user_team_id.rb +1 -1
  26. data/lib/sugarcrm/connection/api/logout.rb +1 -1
  27. data/lib/sugarcrm/connection/api/seamless_login.rb +1 -1
  28. data/lib/sugarcrm/connection/api/search_by_module.rb +1 -1
  29. data/lib/sugarcrm/connection/api/set_campaign_merge.rb +1 -1
  30. data/lib/sugarcrm/connection/api/set_document_revision.rb +1 -1
  31. data/lib/sugarcrm/connection/api/set_entries.rb +1 -1
  32. data/lib/sugarcrm/connection/api/set_entry.rb +1 -1
  33. data/lib/sugarcrm/connection/api/set_note_attachment.rb +1 -1
  34. data/lib/sugarcrm/connection/api/set_relationship.rb +1 -1
  35. data/lib/sugarcrm/connection/api/set_relationships.rb +1 -1
  36. data/lib/sugarcrm/connection/connection.rb +5 -10
  37. data/lib/sugarcrm/connection/helper.rb +3 -2
  38. data/lib/sugarcrm/connection/response.rb +8 -6
  39. data/lib/sugarcrm/exceptions.rb +3 -0
  40. data/lib/sugarcrm/finders.rb +2 -0
  41. data/lib/sugarcrm/{dynamic_finder_match.rb → finders/dynamic_finder_match.rb} +0 -0
  42. data/lib/sugarcrm/finders/finder_methods.rb +236 -0
  43. data/lib/sugarcrm/module.rb +35 -11
  44. data/lib/sugarcrm/module_methods.rb +68 -23
  45. data/lib/sugarcrm/session.rb +179 -0
  46. data/test/connection/test_get_available_modules.rb +1 -4
  47. data/test/connection/test_get_entries.rb +2 -8
  48. data/test/connection/test_get_entry.rb +1 -2
  49. data/test/connection/test_get_entry_list.rb +6 -12
  50. data/test/connection/test_get_module_fields.rb +1 -4
  51. data/test/connection/test_get_relationships.rb +1 -4
  52. data/test/connection/test_get_server_info.rb +1 -4
  53. data/test/connection/test_get_user_id.rb +1 -4
  54. data/test/connection/test_get_user_team_id.rb +1 -4
  55. data/test/connection/test_login.rb +3 -5
  56. data/test/connection/test_logout.rb +1 -4
  57. data/test/connection/test_set_note_attachment.rb +1 -2
  58. data/test/connection/test_set_relationship.rb +1 -2
  59. data/test/helper.rb +6 -8
  60. data/test/test_association_collection.rb +1 -2
  61. data/test/test_associations.rb +18 -1
  62. data/test/test_connection.rb +2 -6
  63. data/test/test_module.rb +34 -6
  64. data/test/test_response.rb +2 -3
  65. data/test/test_session.rb +109 -0
  66. data/test/test_sugarcrm.rb +58 -7
  67. metadata +14 -11
  68. data/lib/sugarcrm/environment.rb +0 -63
  69. data/test/test_environment.rb +0 -45
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Carl Hicks
1
+ Copyright (c) 2011 Carl Hicks
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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 == true
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::Environment.load_config` followed by the absolute path to your configuration file
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::Environment.load_config` followed by the absolute path to your config file. This will log you in automatically ;
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::Environment.extensions_folder = ` followed by the absolute path to the folder containing your extensions
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
- * >= activesupport 3.0.0 gem
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
- Set the values in `test/helper.rb` to point to a SugarCRM instance with demo data
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
@@ -17,6 +17,7 @@ Jeweler::Tasks.new do |gem|
17
17
  gem.email = "carl.hicks@gmail.com"
18
18
  gem.homepage = "http://github.com/chicks/sugarcrm"
19
19
  gem.authors = ["Carl Hicks", "David Sulc"]
20
+ gem.executables = ['sugarcrm']
20
21
  end
21
22
  Jeweler::RubygemsDotOrgTasks.new
22
23
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.10
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/environment'
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
- "#<SugarCRM::Association @proxy_methods=[#{@proxy_methods.join(", ")}], " +
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=#{@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
- return "SugarCRM::#{klass}".constantize if SugarCRM.const_defined? klass
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 "SugarCRM::#{module_name.klass}".constantize if SugarCRM.const_defined? module_name.klass
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 "SugarCRM::#{klass}".constantize if SugarCRM.const_defined? klass
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 = SugarCRM.connection.get_relationships(@owner.class._module.name, @owner.id, @association.to_s)
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 = SugarCRM.connection.set_relationship(
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 = SugarCRM.connection.set_entry(self.class._module.name, serialize_modified_attributes)
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, "SugarCRM::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?
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, if validations pass.
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
- (SugarCRM.connection.set_entry(self.class._module.name, params).class == Hash)
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