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