sugarcrm 0.9.10 → 0.9.11

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.
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
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}"
7
+ "session": "#{@session.id}"
8
8
  }
9
9
  EOF
10
10
 
@@ -12,7 +12,7 @@ module SugarCRM; class Connection
12
12
  mods = send!(:get_available_modules, json)["modules"]
13
13
  modules = []
14
14
  mods.each do |mod|
15
- modules << Module.new(mod)
15
+ modules << Module.new(@session, mod)
16
16
  end
17
17
  modules
18
18
  end
@@ -4,11 +4,11 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "id": #{id}
9
9
  }
10
10
  EOF
11
11
  json.gsub!(/^\s{6}/,'')
12
- SugarCRM::Response.handle(send!(:get_document_revision, json))
12
+ SugarCRM::Response.handle(send!(:get_document_revision, json), @session)
13
13
  end
14
14
  end; end
@@ -10,7 +10,7 @@ module SugarCRM; class Connection
10
10
 
11
11
  json = <<-EOF
12
12
  {
13
- "session": "#{@session}",
13
+ "session": "#{@session.id}",
14
14
  "module_name": "#{module_name}",
15
15
  "ids": #{ids.to_json},
16
16
  "select_fields": #{resolve_fields(module_name, options[:fields])},
@@ -18,6 +18,6 @@ module SugarCRM; class Connection
18
18
  }
19
19
  EOF
20
20
  json.gsub!(/^\s{6}/,'')
21
- SugarCRM::Response.handle(send!(:get_entries, json))
21
+ SugarCRM::Response.handle(send!(:get_entries, json), @session)
22
22
  end
23
23
  end; end
@@ -8,7 +8,7 @@ module SugarCRM; class Connection
8
8
 
9
9
  json = <<-EOF
10
10
  {
11
- "session": "#{@session}",
11
+ "session": "#{@session.id}",
12
12
  "module_name": "#{module_name}",
13
13
  "query": "#{query}",
14
14
  "deleted": #{options[:deleted]}
@@ -9,7 +9,7 @@ module SugarCRM; class Connection
9
9
 
10
10
  json = <<-EOF
11
11
  {
12
- "session": "#{@session}",
12
+ "session": "#{@session.id}",
13
13
  "module_name": "#{module_name}",
14
14
  "id": "#{id}",
15
15
  "select_fields": #{resolve_fields(module_name, options[:fields])},
@@ -18,6 +18,6 @@ module SugarCRM; class Connection
18
18
  EOF
19
19
 
20
20
  json.gsub!(/^\s{6}/,'')
21
- SugarCRM::Response.handle(send!(:get_entry, json))
21
+ SugarCRM::Response.handle(send!(:get_entry, json), @session)
22
22
  end
23
23
  end; end
@@ -14,7 +14,7 @@ module SugarCRM; class Connection
14
14
 
15
15
  json = <<-EOF
16
16
  {
17
- "session": "#{@session}",
17
+ "session": "#{@session.id}",
18
18
  "module_name": "#{module_name}",
19
19
  "query": "#{query}",
20
20
  "order_by": "#{options[:order_by]}",
@@ -26,6 +26,6 @@ module SugarCRM; class Connection
26
26
  }
27
27
  EOF
28
28
  json.gsub!(/^\s{6}/,'')
29
- SugarCRM::Response.handle(send!(:get_entry_list, json))
29
+ SugarCRM::Response.handle(send!(:get_entry_list, json), @session)
30
30
  end
31
31
  end; end
@@ -4,12 +4,12 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "module_name": "#{module_name}"
9
9
  }
10
10
  EOF
11
11
  json.gsub!(/^\s{6}/,'')
12
- SugarCRM::Response.handle(send!(:get_module_fields, json))
12
+ SugarCRM::Response.handle(send!(:get_module_fields, json), @session)
13
13
  end
14
14
  alias :get_fields :get_module_fields
15
15
  end; end
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "id": "#{id}"
9
9
  }
10
10
  EOF
@@ -13,7 +13,7 @@ module SugarCRM; class Connection
13
13
 
14
14
  json = <<-EOF
15
15
  {
16
- "session": "#{@session}",
16
+ "session": "#{@session.id}",
17
17
  "module_name": "#{module_name}",
18
18
  "module_id": "#{id}",
19
19
  "link_field_name": "#{related_to.downcase}",
@@ -24,7 +24,7 @@ module SugarCRM; class Connection
24
24
  }
25
25
  EOF
26
26
  json.gsub!(/^\s{6}/,'')
27
- SugarCRM::Response.new(send!(:get_relationships, json), {:always_return_array => true}).to_obj
27
+ SugarCRM::Response.new(send!(:get_relationships, json), @session, {:always_return_array => true}).to_obj
28
28
  end
29
29
  alias :get_relationship :get_relationships
30
30
  end; end
@@ -6,7 +6,7 @@ module SugarCRM; class Connection
6
6
 
7
7
  json = <<-EOF
8
8
  {
9
- "session": "#{@session}",
9
+ "session": "#{@session.id}",
10
10
  "ids": #{ids.to_json},
11
11
  "select_fields": "#{options[:select_fields].to_json}"
12
12
  }
@@ -2,6 +2,6 @@ module SugarCRM; class Connection
2
2
  # Returns server information such as version, flavor, and gmt_time.
3
3
  def get_server_info
4
4
  login! unless logged_in?
5
- Response.handle(send!(:get_server_info, ""))
5
+ Response.handle(send!(:get_server_info, ""), @session)
6
6
  end
7
7
  end; end
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}"
7
+ "session": "#{@session.id}"
8
8
  }
9
9
  EOF
10
10
  json.gsub!(/^\s{6}/,'')
@@ -5,7 +5,7 @@ module SugarCRM; class Connection
5
5
  login! unless logged_in?
6
6
  json = <<-EOF
7
7
  {
8
- "session": "#{@session}"
8
+ "session": "#{@session.id}"
9
9
  }
10
10
  EOF
11
11
  json.gsub!(/^\s{6}/,'')
@@ -5,7 +5,7 @@ module SugarCRM; class Connection
5
5
  json = <<-EOF
6
6
  {
7
7
  "user_auth": {
8
- "session": "#{@session}"
8
+ "session": "#{@session.id}"
9
9
  }
10
10
  }
11
11
  EOF
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}"
7
+ "session": "#{@session.id}"
8
8
  }
9
9
  EOF
10
10
  json.gsub!(/^\s{6}/,'')
@@ -12,7 +12,7 @@ module SugarCRM; class Connection
12
12
 
13
13
  json = <<-EOF
14
14
  {
15
- "session": "#{@session}",
15
+ "session": "#{@session.id}",
16
16
  "search_string": "#{search_string}",
17
17
  "modules": "#{modules}",
18
18
  "offset": #{options[:offset]},
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "targets": #{targets.to_json},
9
9
  "campaign-id": "#{campaign_id}"
10
10
  }
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "document_revision": "#{revision}",
9
9
  "id": #{id}
10
10
  }
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "module_name": "#{module_name}",
9
9
  "name_value_list": #{name_value_lists.to_json}
10
10
  }
@@ -4,7 +4,7 @@ module SugarCRM; class Connection
4
4
  login! unless logged_in?
5
5
  json = <<-EOF
6
6
  {
7
- "session": "#{@session}",
7
+ "session": "#{@session.id}",
8
8
  "module_name": "#{module_name}",
9
9
  "name_value_list": #{name_value_list.to_json}
10
10
  }
@@ -9,7 +9,7 @@ module SugarCRM; class Connection
9
9
  login! unless logged_in?
10
10
  json = <<-EOF
11
11
  {
12
- "session": "#{@session}",
12
+ "session": "#{@session.id}",
13
13
  "note": {
14
14
  "id": "#{id}",
15
15
  "filename": "#{filename}",
@@ -9,7 +9,7 @@ module SugarCRM; class Connection
9
9
  raise ArgumentError, "related_ids must be an Array" unless related_ids.class == Array
10
10
  json = <<-EOF
11
11
  {
12
- "session": "#{@session}",
12
+ "session": "#{@session.id}",
13
13
  "module_name": "#{module_name}",
14
14
  "module_id": "#{module_id}",
15
15
  "link_field_name": "#{link_field_name}",
@@ -9,7 +9,7 @@ module SugarCRM; class Connection
9
9
 
10
10
  json = <<-EOF
11
11
  {
12
- "session": "#{@session}",
12
+ "session": "#{@session.id}",
13
13
  "module_names": "#{module_names.to_json}",
14
14
  "module_ids": #{module_ids.to_json},
15
15
  "link_field_names": #{link_field_names.to_json},
@@ -8,7 +8,8 @@ module SugarCRM; class Connection
8
8
  attr :url, true
9
9
  attr :user, false
10
10
  attr :pass, false
11
- attr :session, true
11
+ attr :session_id, true
12
+ attr_accessor :session
12
13
  attr :connection, true
13
14
  attr :options, true
14
15
  attr :request, true
@@ -28,26 +29,20 @@ module SugarCRM; class Connection
28
29
  @response = ""
29
30
  resolve_url
30
31
  login!
31
- # make sure the environment singleton gets loaded
32
- if @options[:load_environment] # prevent loops when Environment tries to log in automatically
33
- SugarCRM::Environment.update_config({:base_url => url, :username => user, :password => pass})
34
- end
32
+ @session.update_config({:base_url => url, :username => user, :password => pass}) if @session
35
33
  self
36
34
  end
37
35
 
38
36
  # Check to see if we are logged in
39
37
  def logged_in?
40
38
  connect! unless connected?
41
- @session ? true : false
39
+ @session_id ? true : false
42
40
  end
43
41
 
44
42
  # Login
45
43
  def login!
46
- @session = login["id"]
44
+ @session_id = login["id"]
47
45
  raise SugarCRM::LoginError, "Invalid Login" unless logged_in?
48
- SugarCRM.connection = self
49
- SugarCRM::Base.connection = self
50
- Module.register_all if @options[:register_modules]
51
46
  end
52
47
 
53
48
  # Check to see if we are connected
@@ -18,7 +18,7 @@ module SugarCRM; class Connection
18
18
  # FIXME: This is to work around a bug in SugarCRM 6.0
19
19
  # where no fields are returned if no fields are specified
20
20
  if fields.length == 0
21
- mod = Module.find(module_name.classify)
21
+ mod = Module.find(module_name.classify, @session)
22
22
  if mod
23
23
  fields = mod.fields.keys
24
24
  else
@@ -31,7 +31,8 @@ module SugarCRM; class Connection
31
31
  # Returns an instance of class for the provided module name
32
32
  def class_for(module_name)
33
33
  begin
34
- klass = "SugarCRM::#{module_name.classify}".constantize.new
34
+ class_const = @session.namespace_const.const_get(module_name.classify)
35
+ klass = class_const.new
35
36
  rescue NameError
36
37
  raise InvalidModule, "Module: #{module_name} is not registered"
37
38
  end
@@ -3,8 +3,8 @@ module SugarCRM; class Response
3
3
  # This class handles the response from the server.
4
4
  # It tries to convert the response into an object such as User
5
5
  # or an object collection. If it fails, it just returns the response hash
6
- def handle(json)
7
- r = new(json)
6
+ def handle(json, session)
7
+ r = new(json, session)
8
8
  begin
9
9
  return r.to_obj
10
10
  rescue UninitializedModule => e
@@ -14,7 +14,7 @@ module SugarCRM; class Response
14
14
  rescue InvalidAttributeType => e
15
15
  raise e
16
16
  rescue => e
17
- if SugarCRM.connection.debug?
17
+ if session.connection.debug?
18
18
  puts "Failed to process JSON:"
19
19
  pp json
20
20
  end
@@ -25,10 +25,11 @@ module SugarCRM; class Response
25
25
 
26
26
  attr :response, false
27
27
 
28
- def initialize(json,opts={})
28
+ def initialize(json, session, opts={})
29
29
  @options = { :always_return_array => false }.merge! opts
30
30
  @response = json
31
31
  @response = json.with_indifferent_access if json.is_a? Hash
32
+ @session = session
32
33
  end
33
34
 
34
35
  # Tries to instantiate and return an object with the values
@@ -42,11 +43,12 @@ module SugarCRM; class Response
42
43
  attributes = []
43
44
  _module = resolve_module(object)
44
45
  attributes = flatten_name_value_list(object)
45
- if SugarCRM.const_get(_module)
46
+ namespace = @session.namespace_const
47
+ if namespace.const_get(_module)
46
48
  if attributes.length == 0
47
49
  raise AttributeParsingError, "response contains objects without attributes!"
48
50
  end
49
- objects << SugarCRM.const_get(_module).new(attributes)
51
+ objects << namespace.const_get(_module).new(attributes)
50
52
  else
51
53
  raise InvalidModule, "#{_module} does not exist, or is not accessible"
52
54
  end
@@ -1,5 +1,8 @@
1
1
  module SugarCRM
2
+ class NoActiveSession < RuntimeError; end
3
+ class MultipleSessions < RuntimeError; end
2
4
  class LoginError < RuntimeError; end
5
+ class MissingCredentials < RuntimeError; end
3
6
  class EmptyResponse < RuntimeError; end
4
7
  class UnhandledResponse < RuntimeError; end
5
8
  class InvalidSugarCRMUrl < RuntimeError; end
@@ -0,0 +1,2 @@
1
+ require 'sugarcrm/finders/finder_methods'
2
+ require 'sugarcrm/finders/dynamic_finder_match'
@@ -0,0 +1,236 @@
1
+ module SugarCRM; module FinderMethods
2
+ module ClassMethods
3
+ private
4
+ def find_initial(options)
5
+ options.update(:limit => 1)
6
+ result = find_by_sql(options)
7
+ return result.first if result.instance_of? Array # find_by_sql will return an Array if result are found
8
+ result
9
+ end
10
+
11
+ def find_from_ids(ids, options)
12
+ expects_array = ids.first.kind_of?(Array)
13
+ return ids.first if expects_array && ids.first.empty?
14
+
15
+ ids = ids.flatten.compact.uniq
16
+
17
+ case ids.size
18
+ when 0
19
+ raise RecordNotFound, "Couldn't find #{self._module.name} without an ID"
20
+ when 1
21
+ result = find_one(ids.first, options)
22
+ expects_array ? [ result ] : result
23
+ else
24
+ find_some(ids, options)
25
+ end
26
+ end
27
+
28
+ def find_one(id, options)
29
+
30
+ if result = connection.get_entry(self._module.name, id, {:fields => self._module.fields.keys})
31
+ result
32
+ else
33
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
34
+ end
35
+ end
36
+
37
+ def find_some(ids, options)
38
+ result = connection.get_entries(self._module.name, ids, {:fields => self._module.fields.keys})
39
+
40
+ # Determine expected size from limit and offset, not just ids.size.
41
+ expected_size =
42
+ if options[:limit] && ids.size > options[:limit]
43
+ options[:limit]
44
+ else
45
+ ids.size
46
+ end
47
+
48
+ # 11 ids with limit 3, offset 9 should give 2 results.
49
+ if options[:offset] && (ids.size - options[:offset] < expected_size)
50
+ expected_size = ids.size - options[:offset]
51
+ end
52
+
53
+ if result.size == expected_size
54
+ result
55
+ else
56
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
57
+ end
58
+ end
59
+
60
+ def find_every(options)
61
+ find_by_sql(options)
62
+ end
63
+
64
+ def find_by_sql(options)
65
+ # 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
66
+ # in addition to allowing querying of large datasets while avoiding timeouts,
67
+ # this implementation fixes the :limit - :offset bug so that it behaves correctly
68
+ local_options = {}
69
+ options.keys.each{|k|
70
+ local_options[k] = options[k]
71
+ }
72
+ local_options.delete(:offset) if local_options[:offset] == 0
73
+
74
+ # store the number of records wanted by user, as we'll overwrite :limit option to obtain several slices of records (to avoid timeout issues)
75
+ nb_to_fetch = local_options[:limit]
76
+ nb_to_fetch = nb_to_fetch.to_i if nb_to_fetch
77
+ offset_value = local_options[:offset] || 10 # arbitrary value, must be bigger than :limit used (see comment above)
78
+ offset_value = offset_value.to_i
79
+ offset_value.freeze
80
+ initial_limit = nb_to_fetch.nil? ? offset_value : [offset_value, nb_to_fetch].min # how many records should be fetched on first pass
81
+ # ensure results are ordered so :limit and :offset option behave in a deterministic fashion
82
+ local_options = { :order_by => :id }.merge(local_options)
83
+ local_options.update(:limit => initial_limit) # override original argument
84
+
85
+ # get first slice of results
86
+ # note: to work around a SugarCRM REST API bug, the :limit option must always be smaller than the :offset option
87
+ # 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
88
+ # with :limit always smaller than :offset
89
+ results = connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
90
+ return nil unless results
91
+ results = Array.wrap(results)
92
+
93
+ limit_value = [5, offset_value].min # arbitrary value, must be smaller than :offset used (see comment above)
94
+ limit_value.freeze
95
+ local_options = { :order_by => :id }.merge(local_options)
96
+ local_options.update(:limit => limit_value)
97
+
98
+ # a portion of the results has already been queried
99
+ # update or set the :offset value to reflect this
100
+ local_options[:offset] ||= results.size
101
+ local_options[:offset] += offset_value
102
+
103
+ # continue fetching results until we either
104
+ # a) have as many results as the user wants (specified via the original :limit option)
105
+ # b) there are no more results matching the criteria
106
+ while result_slice = connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
107
+ results.concat(Array.wrap(result_slice))
108
+ # make sure we don't return more results than the user requested (via original :limit option)
109
+ if nb_to_fetch && results.size >= nb_to_fetch
110
+ return results.slice(0, nb_to_fetch)
111
+ end
112
+ local_options[:offset] += local_options[:limit] # update :offset as we get more records
113
+ end
114
+ results
115
+ end
116
+
117
+ def query_from_options(options)
118
+ # If we dont have conditions, just return an empty query
119
+ return "" unless options[:conditions]
120
+ conditions = []
121
+ options[:conditions].each do |condition|
122
+ # Merge the result into the conditions array
123
+ conditions |= flatten_conditions_for(condition)
124
+ end
125
+ conditions.join(" AND ")
126
+ end
127
+
128
+ # return the opposite of the provided order clause
129
+ # this is used for the :last find option
130
+ # in other words SugarCRM::Account.last(:order_by => "name")
131
+ # is equivalent to SugarCRM::Account.first(:order_by => "name DESC")
132
+ def reverse_order_clause(order)
133
+ raise "reversing multiple order clauses not supported" if order.split(',').size > 1
134
+ raise "order clause format not understood; expected 'column_name (ASC|DESC)?'" unless order =~ /^\s*(\S+)\s*(ASC|DESC)?\s*$/
135
+ column_name = $1
136
+ reversed_order = {'ASC' => 'DESC', 'DESC' => 'ASC'}[$2 || 'ASC']
137
+ return "#{column_name} #{reversed_order}"
138
+ end
139
+
140
+ # Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
141
+ # that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
142
+ # <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
143
+ # <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
144
+ #
145
+ # It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
146
+ # is actually <tt>find_all_by_amount(amount, options)</tt>.
147
+ #
148
+ # Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
149
+ # are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
150
+ # respectively.
151
+ #
152
+ # Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
153
+ # attempts to use it do not run through method_missing.
154
+ def method_missing(method_id, *arguments, &block)
155
+ if match = DynamicFinderMatch.match(method_id)
156
+ attribute_names = match.attribute_names
157
+ super unless all_attributes_exists?(attribute_names)
158
+ if match.finder?
159
+ finder = match.finder
160
+ bang = match.bang?
161
+ self.class_eval <<-EOS, __FILE__, __LINE__ + 1
162
+ def self.#{method_id}(*args)
163
+ options = args.extract_options!
164
+ attributes = construct_attributes_from_arguments(
165
+ [:#{attribute_names.join(',:')}],
166
+ args
167
+ )
168
+ finder_options = { :conditions => attributes }
169
+ validate_find_options(options)
170
+
171
+ #{'result = ' if bang}if options[:conditions]
172
+ with_scope(:find => finder_options) do
173
+ find(:#{finder}, options)
174
+ end
175
+ else
176
+ find(:#{finder}, options.merge(finder_options))
177
+ end
178
+ #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
179
+ end
180
+ EOS
181
+ send(method_id, *arguments)
182
+ elsif match.instantiator?
183
+ instantiator = match.instantiator
184
+ self.class_eval <<-EOS, __FILE__, __LINE__ + 1
185
+ def self.#{method_id}(*args)
186
+ attributes = [:#{attribute_names.join(',:')}]
187
+ protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
188
+ args.each_with_index do |arg, i|
189
+ if arg.is_a?(Hash)
190
+ protected_attributes_for_create = args[i].with_indifferent_access
191
+ else
192
+ unprotected_attributes_for_create[attributes[i]] = args[i]
193
+ end
194
+ end
195
+
196
+ find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
197
+
198
+ options = { :conditions => find_attributes }
199
+
200
+ record = find(:first, options)
201
+
202
+ if record.nil?
203
+ record = self.new(unprotected_attributes_for_create)
204
+ #{'record.save' if instantiator == :create}
205
+ record
206
+ else
207
+ record
208
+ end
209
+ end
210
+ EOS
211
+ send(method_id, *arguments, &block)
212
+ end
213
+ else
214
+ super
215
+ end
216
+ end
217
+
218
+ def all_attributes_exists?(attribute_names)
219
+ attribute_names.all? { |name| attributes_from_module.include?(name) }
220
+ end
221
+
222
+ def construct_attributes_from_arguments(attribute_names, arguments)
223
+ attributes = {}
224
+ attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
225
+ attributes
226
+ end
227
+
228
+ VALID_FIND_OPTIONS = [ :conditions, :deleted, :fields, :include, :joins, :limit, :link_fields, :offset,
229
+ :order_by, :select, :readonly, :group, :having, :from, :lock ]
230
+
231
+ def validate_find_options(options) #:nodoc:
232
+ options.assert_valid_keys(VALID_FIND_OPTIONS)
233
+ end
234
+ end
235
+ end
236
+ end