sugarcrm 0.9.10 → 0.9.11
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.rdoc +38 -6
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/sugarcrm +26 -0
- data/lib/sugarcrm.rb +2 -2
- data/lib/sugarcrm/associations/association.rb +11 -8
- data/lib/sugarcrm/associations/association_collection.rb +1 -1
- data/lib/sugarcrm/associations/association_methods.rb +1 -1
- data/lib/sugarcrm/attributes/attribute_methods.rb +7 -2
- data/lib/sugarcrm/attributes/attribute_typecast.rb +2 -2
- data/lib/sugarcrm/base.rb +37 -236
- data/lib/sugarcrm/connection/api/get_available_modules.rb +2 -2
- data/lib/sugarcrm/connection/api/get_document_revision.rb +2 -2
- data/lib/sugarcrm/connection/api/get_entries.rb +2 -2
- data/lib/sugarcrm/connection/api/get_entries_count.rb +1 -1
- data/lib/sugarcrm/connection/api/get_entry.rb +2 -2
- data/lib/sugarcrm/connection/api/get_entry_list.rb +2 -2
- data/lib/sugarcrm/connection/api/get_module_fields.rb +2 -2
- data/lib/sugarcrm/connection/api/get_note_attachment.rb +1 -1
- data/lib/sugarcrm/connection/api/get_relationships.rb +2 -2
- data/lib/sugarcrm/connection/api/get_report_entries.rb +1 -1
- data/lib/sugarcrm/connection/api/get_server_info.rb +1 -1
- data/lib/sugarcrm/connection/api/get_user_id.rb +1 -1
- data/lib/sugarcrm/connection/api/get_user_team_id.rb +1 -1
- data/lib/sugarcrm/connection/api/logout.rb +1 -1
- data/lib/sugarcrm/connection/api/seamless_login.rb +1 -1
- data/lib/sugarcrm/connection/api/search_by_module.rb +1 -1
- data/lib/sugarcrm/connection/api/set_campaign_merge.rb +1 -1
- data/lib/sugarcrm/connection/api/set_document_revision.rb +1 -1
- data/lib/sugarcrm/connection/api/set_entries.rb +1 -1
- data/lib/sugarcrm/connection/api/set_entry.rb +1 -1
- data/lib/sugarcrm/connection/api/set_note_attachment.rb +1 -1
- data/lib/sugarcrm/connection/api/set_relationship.rb +1 -1
- data/lib/sugarcrm/connection/api/set_relationships.rb +1 -1
- data/lib/sugarcrm/connection/connection.rb +5 -10
- data/lib/sugarcrm/connection/helper.rb +3 -2
- data/lib/sugarcrm/connection/response.rb +8 -6
- data/lib/sugarcrm/exceptions.rb +3 -0
- data/lib/sugarcrm/finders.rb +2 -0
- data/lib/sugarcrm/{dynamic_finder_match.rb → finders/dynamic_finder_match.rb} +0 -0
- data/lib/sugarcrm/finders/finder_methods.rb +236 -0
- data/lib/sugarcrm/module.rb +35 -11
- data/lib/sugarcrm/module_methods.rb +68 -23
- data/lib/sugarcrm/session.rb +179 -0
- data/test/connection/test_get_available_modules.rb +1 -4
- data/test/connection/test_get_entries.rb +2 -8
- data/test/connection/test_get_entry.rb +1 -2
- data/test/connection/test_get_entry_list.rb +6 -12
- data/test/connection/test_get_module_fields.rb +1 -4
- data/test/connection/test_get_relationships.rb +1 -4
- data/test/connection/test_get_server_info.rb +1 -4
- data/test/connection/test_get_user_id.rb +1 -4
- data/test/connection/test_get_user_team_id.rb +1 -4
- data/test/connection/test_login.rb +3 -5
- data/test/connection/test_logout.rb +1 -4
- data/test/connection/test_set_note_attachment.rb +1 -2
- data/test/connection/test_set_relationship.rb +1 -2
- data/test/helper.rb +6 -8
- data/test/test_association_collection.rb +1 -2
- data/test/test_associations.rb +18 -1
- data/test/test_connection.rb +2 -6
- data/test/test_module.rb +34 -6
- data/test/test_response.rb +2 -3
- data/test/test_session.rb +109 -0
- data/test/test_sugarcrm.rb +58 -7
- metadata +14 -11
- data/lib/sugarcrm/environment.rb +0 -63
- 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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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}",
|
@@ -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 :
|
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
|
-
|
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
|
-
@
|
39
|
+
@session_id ? true : false
|
42
40
|
end
|
43
41
|
|
44
42
|
# Login
|
45
43
|
def login!
|
46
|
-
@
|
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
|
-
|
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
|
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
|
-
|
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 <<
|
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
|
data/lib/sugarcrm/exceptions.rb
CHANGED
@@ -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
|
File without changes
|
@@ -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
|