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