sugarcrm_emp 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +29 -0
- data/Gemfile +14 -0
- data/LICENSE +20 -0
- data/README.rdoc +275 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/WATCHLIST.rdoc +7 -0
- data/bin/sugarcrm +26 -0
- data/lib/rails/generators/sugarcrm/config/config_generator.rb +22 -0
- data/lib/rails/generators/sugarcrm/config/templates/initializer.rb +4 -0
- data/lib/rails/generators/sugarcrm/config/templates/sugarcrm.yml +19 -0
- data/lib/sugarcrm/associations/association.rb +170 -0
- data/lib/sugarcrm/associations/association_cache.rb +36 -0
- data/lib/sugarcrm/associations/association_collection.rb +141 -0
- data/lib/sugarcrm/associations/association_methods.rb +91 -0
- data/lib/sugarcrm/associations/associations.rb +61 -0
- data/lib/sugarcrm/associations.rb +5 -0
- data/lib/sugarcrm/attributes/attribute_methods.rb +203 -0
- data/lib/sugarcrm/attributes/attribute_serializers.rb +55 -0
- data/lib/sugarcrm/attributes/attribute_typecast.rb +44 -0
- data/lib/sugarcrm/attributes/attribute_validations.rb +62 -0
- data/lib/sugarcrm/attributes.rb +4 -0
- data/lib/sugarcrm/base.rb +355 -0
- data/lib/sugarcrm/config/sugarcrm.yaml +10 -0
- data/lib/sugarcrm/connection/api/get_available_modules.rb +22 -0
- data/lib/sugarcrm/connection/api/get_document_revision.rb +14 -0
- data/lib/sugarcrm/connection/api/get_entries.rb +23 -0
- data/lib/sugarcrm/connection/api/get_entries_count.rb +20 -0
- data/lib/sugarcrm/connection/api/get_entry.rb +23 -0
- data/lib/sugarcrm/connection/api/get_entry_list.rb +31 -0
- data/lib/sugarcrm/connection/api/get_module_fields.rb +15 -0
- data/lib/sugarcrm/connection/api/get_note_attachment.rb +14 -0
- data/lib/sugarcrm/connection/api/get_relationships.rb +30 -0
- data/lib/sugarcrm/connection/api/get_report_entries.rb +17 -0
- data/lib/sugarcrm/connection/api/get_server_info.rb +7 -0
- data/lib/sugarcrm/connection/api/get_user_id.rb +13 -0
- data/lib/sugarcrm/connection/api/get_user_team_id.rb +14 -0
- data/lib/sugarcrm/connection/api/login.rb +18 -0
- data/lib/sugarcrm/connection/api/logout.rb +15 -0
- data/lib/sugarcrm/connection/api/seamless_login.rb +13 -0
- data/lib/sugarcrm/connection/api/search_by_module.rb +25 -0
- data/lib/sugarcrm/connection/api/set_campaign_merge.rb +15 -0
- data/lib/sugarcrm/connection/api/set_document_revision.rb +35 -0
- data/lib/sugarcrm/connection/api/set_entries.rb +15 -0
- data/lib/sugarcrm/connection/api/set_entry.rb +15 -0
- data/lib/sugarcrm/connection/api/set_note_attachment.rb +25 -0
- data/lib/sugarcrm/connection/api/set_relationship.rb +27 -0
- data/lib/sugarcrm/connection/api/set_relationships.rb +22 -0
- data/lib/sugarcrm/connection/connection.rb +201 -0
- data/lib/sugarcrm/connection/helper.rb +50 -0
- data/lib/sugarcrm/connection/request.rb +61 -0
- data/lib/sugarcrm/connection/response.rb +91 -0
- data/lib/sugarcrm/connection.rb +5 -0
- data/lib/sugarcrm/connection_pool.rb +163 -0
- data/lib/sugarcrm/exceptions.rb +23 -0
- data/lib/sugarcrm/extensions/README.txt +23 -0
- data/lib/sugarcrm/finders/dynamic_finder_match.rb +41 -0
- data/lib/sugarcrm/finders/finder_methods.rb +243 -0
- data/lib/sugarcrm/finders.rb +2 -0
- data/lib/sugarcrm/module.rb +174 -0
- data/lib/sugarcrm/module_methods.rb +91 -0
- data/lib/sugarcrm/session.rb +218 -0
- data/lib/sugarcrm.rb +22 -0
- data/sugarcrm.gemspec +178 -0
- data/test/config_test.yaml +15 -0
- data/test/connection/test_get_available_modules.rb +9 -0
- data/test/connection/test_get_entries.rb +15 -0
- data/test/connection/test_get_entry.rb +22 -0
- data/test/connection/test_get_entry_list.rb +23 -0
- data/test/connection/test_get_module_fields.rb +11 -0
- data/test/connection/test_get_relationships.rb +12 -0
- data/test/connection/test_get_server_info.rb +9 -0
- data/test/connection/test_get_user_id.rb +9 -0
- data/test/connection/test_get_user_team_id.rb +9 -0
- data/test/connection/test_login.rb +9 -0
- data/test/connection/test_logout.rb +9 -0
- data/test/connection/test_set_document_revision.rb +28 -0
- data/test/connection/test_set_entry.rb +15 -0
- data/test/connection/test_set_note_attachment.rb +16 -0
- data/test/connection/test_set_relationship.rb +18 -0
- data/test/extensions_test/patch.rb +9 -0
- data/test/helper.rb +17 -0
- data/test/test_association_collection.rb +11 -0
- data/test/test_associations.rb +156 -0
- data/test/test_connection.rb +13 -0
- data/test/test_connection_pool.rb +40 -0
- data/test/test_finders.rb +201 -0
- data/test/test_module.rb +51 -0
- data/test/test_request.rb +35 -0
- data/test/test_response.rb +26 -0
- data/test/test_session.rb +136 -0
- data/test/test_sugarcrm.rb +213 -0
- metadata +266 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module SugarCRM; class ConnectionPool
|
4
|
+
attr_accessor :timeout
|
5
|
+
attr_reader :size
|
6
|
+
def initialize(session)
|
7
|
+
@session = session
|
8
|
+
|
9
|
+
# The cache of reserved connections mapped to threads
|
10
|
+
@reserved_connections = {}
|
11
|
+
|
12
|
+
# The mutex used to synchronize pool access
|
13
|
+
@connection_mutex = Monitor.new
|
14
|
+
@queue = @connection_mutex.new_cond
|
15
|
+
@timeout = config_timeout || 10
|
16
|
+
|
17
|
+
# default max pool size to 5
|
18
|
+
@size = config_pool_size || default_pool_size
|
19
|
+
|
20
|
+
@connections = []
|
21
|
+
@checked_out = []
|
22
|
+
end
|
23
|
+
|
24
|
+
# If a connection already exists yield it to the block. If no connection
|
25
|
+
# exists checkout a connection, yield it to the block, and checkin the
|
26
|
+
# connection when finished.
|
27
|
+
def with_connection
|
28
|
+
connection_id = current_connection_id
|
29
|
+
fresh_connection = true unless @reserved_connections[connection_id]
|
30
|
+
yield connection
|
31
|
+
ensure
|
32
|
+
release_connection(connection_id) if fresh_connection
|
33
|
+
end
|
34
|
+
|
35
|
+
# Retrieve the connection associated with the current thread, or call
|
36
|
+
# #checkout to obtain one if necessary.
|
37
|
+
#
|
38
|
+
# #connection can be called any number of times; the connection is
|
39
|
+
# held in a hash keyed by the thread id.
|
40
|
+
def connection
|
41
|
+
@reserved_connections[current_connection_id] ||= checkout
|
42
|
+
end
|
43
|
+
|
44
|
+
# Check-out a sugarcrm connection from the pool, indicating that you want
|
45
|
+
# to use it. You should call #checkin when you no longer need this.
|
46
|
+
#
|
47
|
+
# This is done by either returning an existing connection, or by creating
|
48
|
+
# a new connection. If the maximum number of connections for this pool has
|
49
|
+
# already been reached, but the pool is empty (i.e. they're all being used),
|
50
|
+
# then this method will wait until a thread has checked in a connection.
|
51
|
+
# The wait time is bounded however: if no connection can be checked out
|
52
|
+
# within the timeout specified for this pool, then a ConnectionTimeoutError
|
53
|
+
# exception will be raised.
|
54
|
+
def checkout
|
55
|
+
# Checkout an available connection
|
56
|
+
@connection_mutex.synchronize do
|
57
|
+
loop do
|
58
|
+
conn = if @checked_out.size < @connections.size
|
59
|
+
checkout_existing_connection
|
60
|
+
elsif @connections.size < @size
|
61
|
+
checkout_new_connection
|
62
|
+
end
|
63
|
+
return conn if conn
|
64
|
+
|
65
|
+
@queue.wait(@timeout)
|
66
|
+
|
67
|
+
if(@checked_out.size < @connections.size)
|
68
|
+
next
|
69
|
+
else
|
70
|
+
clear_stale_cached_connections!
|
71
|
+
if @size == @checked_out.size
|
72
|
+
raise SugarCRM::ConnectionTimeoutError, "could not obtain a sugarcrm connection#{" within #{@timeout} seconds" if @timeout}. The max pool size is currently #{@size}; consider increasing it."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Check-in a sugarcrm connection back into the pool, indicating that you
|
81
|
+
# no longer need this connection.
|
82
|
+
def checkin(conn)
|
83
|
+
@connection_mutex.synchronize do
|
84
|
+
@checked_out.delete conn
|
85
|
+
@queue.signal
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Disconnects all connections in the pool, and clears the pool.
|
90
|
+
def disconnect!
|
91
|
+
@reserved_connections.each_value do |conn|
|
92
|
+
checkin conn
|
93
|
+
end
|
94
|
+
@reserved_connections = {}
|
95
|
+
@connections.each do |conn|
|
96
|
+
conn.logout
|
97
|
+
end
|
98
|
+
@connections = []
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return any checked-out connections back to the pool by threads that
|
102
|
+
# are no longer alive.
|
103
|
+
def clear_stale_cached_connections!
|
104
|
+
keys = @reserved_connections.keys - Thread.list.find_all { |t|
|
105
|
+
t.alive?
|
106
|
+
}.map { |thread| thread.object_id }
|
107
|
+
keys.each do |key|
|
108
|
+
checkin @reserved_connections[key]
|
109
|
+
@reserved_connections.delete(key)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
def new_connection
|
115
|
+
c = Connection.new(@session.config[:base_url], @session.config[:username], @session.config[:password], @session.config[:options])
|
116
|
+
c.session = @session
|
117
|
+
c
|
118
|
+
end
|
119
|
+
|
120
|
+
def checkout_new_connection
|
121
|
+
c = new_connection
|
122
|
+
@connections << c
|
123
|
+
checkout_connection(c)
|
124
|
+
end
|
125
|
+
|
126
|
+
def checkout_existing_connection
|
127
|
+
c = (@connections - @checked_out).first
|
128
|
+
checkout_connection(c)
|
129
|
+
end
|
130
|
+
|
131
|
+
def checkout_connection(c)
|
132
|
+
@checked_out << c
|
133
|
+
c
|
134
|
+
end
|
135
|
+
|
136
|
+
def current_connection_id #:nodoc:
|
137
|
+
Thread.current.object_id
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns the connection pool timeout, if present
|
141
|
+
def config_timeout
|
142
|
+
begin
|
143
|
+
@session.config[:options][:connection_pool][:wait_timeout] && @session.config[:options][:connection_pool][:wait_timeout].to_i
|
144
|
+
rescue
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns the connection pool size, if present
|
150
|
+
def config_pool_size
|
151
|
+
begin
|
152
|
+
@session.config[:options][:connection_pool][:size] && @session.config[:options][:connection_pool][:size].to_i
|
153
|
+
rescue
|
154
|
+
false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# The default for the connection pool's maximum size depends on environment:
|
159
|
+
# default pool size will be 1 unless used within Rails
|
160
|
+
def default_pool_size
|
161
|
+
defined?(Rails) ? 5 : 1
|
162
|
+
end
|
163
|
+
end; end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SugarCRM
|
2
|
+
class NoActiveSession < RuntimeError; end
|
3
|
+
class MultipleSessions < RuntimeError; end
|
4
|
+
class InvalidSession < RuntimeError; end
|
5
|
+
class RetryLimitExceeded < RuntimeError; end
|
6
|
+
class LoginError < RuntimeError; end
|
7
|
+
class MissingCredentials < RuntimeError; end
|
8
|
+
class ConnectionTimeoutError < RuntimeError; end
|
9
|
+
class EmptyResponse < RuntimeError; end
|
10
|
+
class UnhandledResponse < RuntimeError; end
|
11
|
+
class InvalidSugarCRMUrl < RuntimeError; end
|
12
|
+
class InvalidRequest < RuntimeError; end
|
13
|
+
class InvalidModule <RuntimeError; end
|
14
|
+
class AttributeParsingError < RuntimeError; end
|
15
|
+
class RecordNotFound < RuntimeError; end
|
16
|
+
class InvalidRecord < RuntimeError; end
|
17
|
+
class RecordSaveFailed < RuntimeError; end
|
18
|
+
class AssociationFailed < RuntimeError; end
|
19
|
+
class UninitializedModule < RuntimeError; end
|
20
|
+
class InvalidAttribute < RuntimeError; end
|
21
|
+
class InvalidAttributeType < RuntimeError; end
|
22
|
+
class InvalidAssociation < RuntimeError; end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Include your extension files here, as simple *.rb files. Here is an example of an extension:
|
2
|
+
|
3
|
+
SugarCRM::Contact.class_eval do
|
4
|
+
def self.ten_oldest
|
5
|
+
self.all(:order_by => 'date_entered', :limit => 10)
|
6
|
+
end
|
7
|
+
|
8
|
+
def vip?
|
9
|
+
self.opportunities.size > 100
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# This will enable you to call
|
14
|
+
|
15
|
+
SugarCRM::Contact.ten_oldest
|
16
|
+
|
17
|
+
# to get the 10 oldest contacts entered in CRM .
|
18
|
+
#
|
19
|
+
# You will also be able to call
|
20
|
+
|
21
|
+
SugarCRM::Contact.first.vip?
|
22
|
+
|
23
|
+
# to see whether a contact is VIP or not.
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SugarCRM
|
2
|
+
class DynamicFinderMatch
|
3
|
+
def self.match(method)
|
4
|
+
df_match = self.new(method)
|
5
|
+
df_match.finder ? df_match : nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(method)
|
9
|
+
@finder = :first
|
10
|
+
case method.to_s
|
11
|
+
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
12
|
+
@finder = :last if $1 == 'last_by'
|
13
|
+
@finder = :all if $1 == 'all_by'
|
14
|
+
names = $2
|
15
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
16
|
+
@bang = true
|
17
|
+
names = $1
|
18
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
19
|
+
@instantiator = $1 == 'initialize' ? :new : :create
|
20
|
+
names = $2
|
21
|
+
else
|
22
|
+
@finder = nil
|
23
|
+
end
|
24
|
+
@attribute_names = names && names.split('_and_')
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :finder, :attribute_names, :instantiator
|
28
|
+
|
29
|
+
def finder?
|
30
|
+
!@finder.nil? && @instantiator.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def instantiator?
|
34
|
+
@finder == :first && !@instantiator.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def bang?
|
38
|
+
@bang
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,243 @@
|
|
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, &block)
|
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, &block)
|
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, &block)
|
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
|
+
if block_given?
|
55
|
+
result.each{|r|
|
56
|
+
yield r
|
57
|
+
}
|
58
|
+
end
|
59
|
+
result
|
60
|
+
else
|
61
|
+
raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_every(options, &block)
|
66
|
+
find_by_sql(options, &block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# the number of records we retrieve with each query
|
70
|
+
# it is kept small to avoid timeout issues
|
71
|
+
SLICE_SIZE = 5
|
72
|
+
SLICE_SIZE.freeze
|
73
|
+
# results accumulator stores the results we have fetched so far, recursively
|
74
|
+
def find_by_sql(options, results_accumulator=nil, &block)
|
75
|
+
# SugarCRM REST API has a bug (fixed in release _6.4.0.patch as indicated in SugarCRM bug number 43338)
|
76
|
+
# where, when :limit and :offset options are passed simultaneously,
|
77
|
+
# :limit is considered to be the smallest of the two, and :offset is the larger
|
78
|
+
# In addition to allowing querying of large datasets while avoiding timeouts (by fetching results in small slices),
|
79
|
+
# this implementation fixes the :limit - :offset bug so that it behaves correctly
|
80
|
+
|
81
|
+
offset = options[:offset].to_i >= 1 ? options[:offset].to_i : nil
|
82
|
+
|
83
|
+
# if many results are requested (i.e. multiple result slices), we call this function recursively
|
84
|
+
# this array keeps track of which slice we are retrieving (by updating the :offset and :limit options)
|
85
|
+
local_options = {}
|
86
|
+
# ensure results are ordered so :limit and :offset option behave in a deterministic fashion
|
87
|
+
local_options[:order_by] = :id unless options[:order_by]
|
88
|
+
|
89
|
+
# we must ensure limit <= offset (due to bug mentioned above)
|
90
|
+
if offset
|
91
|
+
local_options[:limit] = [offset.to_i, SLICE_SIZE].min
|
92
|
+
local_options[:offset] = offset if offset
|
93
|
+
else
|
94
|
+
local_options[:limit] = options[:limit] ? [options[:limit].to_i, SLICE_SIZE].min : SLICE_SIZE
|
95
|
+
end
|
96
|
+
local_options[:limit] = [local_options[:limit], options[:limit]].min if options[:limit] # don't retrieve more records than required
|
97
|
+
local_options = options.merge(local_options)
|
98
|
+
|
99
|
+
query = query_from_options(local_options)
|
100
|
+
result_slice = connection.get_entry_list(self._module.name, query, local_options)
|
101
|
+
return results_accumulator unless result_slice
|
102
|
+
|
103
|
+
result_slice_array = Array.wrap(result_slice)
|
104
|
+
if block_given?
|
105
|
+
result_slice_array.each{|r| yield r }
|
106
|
+
else
|
107
|
+
results_accumulator = [] unless results_accumulator
|
108
|
+
results_accumulator = results_accumulator.concat(result_slice_array)
|
109
|
+
end
|
110
|
+
|
111
|
+
# adjust options to take into account records that were already retrieved
|
112
|
+
updated_options = {:offset => options[:offset].to_i + result_slice_array.size}
|
113
|
+
updated_options[:limit] = (options[:limit] ? options[:limit] - result_slice_array.size : nil)
|
114
|
+
updated_options = options.merge(updated_options)
|
115
|
+
|
116
|
+
# have we retrieved all the records?
|
117
|
+
if (updated_options[:limit] && updated_options[:limit] < 1) || local_options[:limit] > result_slice_array.size
|
118
|
+
return results_accumulator
|
119
|
+
else
|
120
|
+
find_by_sql(updated_options, results_accumulator, &block)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def query_from_options(options)
|
125
|
+
# If we dont have conditions, just return an empty query
|
126
|
+
return "" unless options[:conditions]
|
127
|
+
conditions = []
|
128
|
+
options[:conditions].each do |condition|
|
129
|
+
# Merge the result into the conditions array
|
130
|
+
conditions |= flatten_conditions_for(condition)
|
131
|
+
end
|
132
|
+
conditions.join(" AND ")
|
133
|
+
end
|
134
|
+
|
135
|
+
# return the opposite of the provided order clause
|
136
|
+
# this is used for the :last find option
|
137
|
+
# in other words SugarCRM::Account.last(:order_by => "name")
|
138
|
+
# is equivalent to SugarCRM::Account.first(:order_by => "name DESC")
|
139
|
+
def reverse_order_clause(order)
|
140
|
+
raise "reversing multiple order clauses not supported" if order.split(',').size > 1
|
141
|
+
raise "order clause format not understood; expected 'column_name (ASC|DESC)?'" unless order =~ /^\s*(\S+)\s*(ASC|DESC)?\s*$/
|
142
|
+
column_name = $1
|
143
|
+
reversed_order = {'ASC' => 'DESC', 'DESC' => 'ASC'}[$2 || 'ASC']
|
144
|
+
return "#{column_name} #{reversed_order}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
|
148
|
+
# that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
|
149
|
+
# <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
|
150
|
+
# <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
|
151
|
+
#
|
152
|
+
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
|
153
|
+
# is actually <tt>find_all_by_amount(amount, options)</tt>.
|
154
|
+
#
|
155
|
+
# Also enables dynamic scopes like scoped_by_user_name(user_name) and scoped_by_user_name_and_password(user_name, password) that
|
156
|
+
# are turned into scoped(:conditions => ["user_name = ?", user_name]) and scoped(:conditions => ["user_name = ? AND password = ?", user_name, password])
|
157
|
+
# respectively.
|
158
|
+
#
|
159
|
+
# Each dynamic finder, scope or initializer/creator is also defined in the class after it is first invoked, so that future
|
160
|
+
# attempts to use it do not run through method_missing.
|
161
|
+
def method_missing(method_id, *arguments, &block)
|
162
|
+
if match = DynamicFinderMatch.match(method_id)
|
163
|
+
attribute_names = match.attribute_names
|
164
|
+
super unless all_attributes_exists?(attribute_names)
|
165
|
+
if match.finder?
|
166
|
+
finder = match.finder
|
167
|
+
bang = match.bang?
|
168
|
+
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
169
|
+
def self.#{method_id}(*args)
|
170
|
+
options = args.extract_options!
|
171
|
+
attributes = construct_attributes_from_arguments(
|
172
|
+
[:#{attribute_names.join(',:')}],
|
173
|
+
args
|
174
|
+
)
|
175
|
+
finder_options = { :conditions => attributes }
|
176
|
+
validate_find_options(options)
|
177
|
+
|
178
|
+
#{'result = ' if bang}if options[:conditions]
|
179
|
+
with_scope(:find => finder_options) do
|
180
|
+
find(:#{finder}, options)
|
181
|
+
end
|
182
|
+
else
|
183
|
+
find(:#{finder}, options.merge(finder_options))
|
184
|
+
end
|
185
|
+
#{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect {|pair| "#{pair.first} = #{pair.second}"}.join(\', \')}")' if bang}
|
186
|
+
end
|
187
|
+
EOS
|
188
|
+
send(method_id, *arguments)
|
189
|
+
elsif match.instantiator?
|
190
|
+
instantiator = match.instantiator
|
191
|
+
self.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
192
|
+
def self.#{method_id}(*args)
|
193
|
+
attributes = [:#{attribute_names.join(',:')}]
|
194
|
+
protected_attributes_for_create, unprotected_attributes_for_create = {}, {}
|
195
|
+
args.each_with_index do |arg, i|
|
196
|
+
if arg.is_a?(Hash)
|
197
|
+
protected_attributes_for_create = args[i].with_indifferent_access
|
198
|
+
else
|
199
|
+
unprotected_attributes_for_create[attributes[i]] = args[i]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
find_attributes = (protected_attributes_for_create.merge(unprotected_attributes_for_create)).slice(*attributes)
|
204
|
+
|
205
|
+
options = { :conditions => find_attributes }
|
206
|
+
|
207
|
+
record = find(:first, options)
|
208
|
+
|
209
|
+
if record.nil?
|
210
|
+
record = self.new(unprotected_attributes_for_create)
|
211
|
+
#{'record.save' if instantiator == :create}
|
212
|
+
record
|
213
|
+
else
|
214
|
+
record
|
215
|
+
end
|
216
|
+
end
|
217
|
+
EOS
|
218
|
+
send(method_id, *arguments, &block)
|
219
|
+
end
|
220
|
+
else
|
221
|
+
super
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def all_attributes_exists?(attribute_names)
|
226
|
+
attribute_names.all? { |name| attributes_from_module.include?(name) }
|
227
|
+
end
|
228
|
+
|
229
|
+
def construct_attributes_from_arguments(attribute_names, arguments)
|
230
|
+
attributes = {}
|
231
|
+
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
|
232
|
+
attributes
|
233
|
+
end
|
234
|
+
|
235
|
+
VALID_FIND_OPTIONS = [ :conditions, :deleted, :fields, :include, :joins, :limit, :link_fields, :offset,
|
236
|
+
:order_by, :select, :readonly, :group, :having, :from, :lock ]
|
237
|
+
|
238
|
+
def validate_find_options(options) #:nodoc:
|
239
|
+
options.assert_valid_keys(VALID_FIND_OPTIONS)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module SugarCRM
|
2
|
+
# A class for handling SugarCRM Modules
|
3
|
+
class Module
|
4
|
+
attr :name, true
|
5
|
+
attr :table_name, true
|
6
|
+
attr :custom_table_name, true
|
7
|
+
attr :klass, true
|
8
|
+
attr :fields, true
|
9
|
+
attr :link_fields, true
|
10
|
+
alias :bean :klass
|
11
|
+
|
12
|
+
# Dynamically register objects based on Module name
|
13
|
+
# I.e. a SugarCRM Module named Users will generate
|
14
|
+
# a SugarCRM::User class.
|
15
|
+
def initialize(session, name)
|
16
|
+
@session = session # the session from which this module was retrieved
|
17
|
+
@name = name
|
18
|
+
@klass = name.classify
|
19
|
+
unless custom_module?
|
20
|
+
@table_name = name.tableize
|
21
|
+
else
|
22
|
+
@table_name = @name
|
23
|
+
end
|
24
|
+
@custom_table_name = resolve_custom_table_name
|
25
|
+
@fields = {}
|
26
|
+
@link_fields = {}
|
27
|
+
@fields_registered = false
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return true if this module was created in the SugarCRM Studio (i.e. it is not part of the modules that
|
32
|
+
# ship in the default SugarCRM configuration)
|
33
|
+
def custom_module?
|
34
|
+
# custom module names are all lower_case, whereas SugarCRM modules are CamelCase
|
35
|
+
@name.downcase == @name
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set table name for custom attibutes
|
39
|
+
# Custom attributes are contained in a table named after the module, with a '_cstm' suffix.
|
40
|
+
# The module's table name must be tableized for the modules that ship with SugarCRM.
|
41
|
+
# For custom modules (created in the Studio), table name don't need to be tableized since
|
42
|
+
# the name passed to the constructor is already tableized
|
43
|
+
def resolve_custom_table_name
|
44
|
+
if custom_module?
|
45
|
+
@custom_table_name = @name + "_cstm"
|
46
|
+
else
|
47
|
+
@custom_table_name = @table_name + "_cstm"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the fields associated with the module
|
52
|
+
def fields
|
53
|
+
return @fields if fields_registered?
|
54
|
+
all_fields = @session.connection.get_fields(@name)
|
55
|
+
@fields = all_fields["module_fields"].with_indifferent_access
|
56
|
+
@link_fields= all_fields["link_fields"]
|
57
|
+
handle_empty_arrays
|
58
|
+
@fields_registered = true
|
59
|
+
@fields
|
60
|
+
end
|
61
|
+
|
62
|
+
def fields_registered?
|
63
|
+
@fields_registered
|
64
|
+
end
|
65
|
+
|
66
|
+
alias :link_fields_registered? :fields_registered?
|
67
|
+
|
68
|
+
# Returns the required fields
|
69
|
+
def required_fields
|
70
|
+
required_fields = []
|
71
|
+
ignore_fields = [:id, :date_entered, :date_modified]
|
72
|
+
self.fields.each_value do |field|
|
73
|
+
next if ignore_fields.include? field["name"].to_sym
|
74
|
+
required_fields << field["name"].to_sym if field["required"] == 1
|
75
|
+
end
|
76
|
+
required_fields
|
77
|
+
end
|
78
|
+
|
79
|
+
def link_fields
|
80
|
+
self.fields unless link_fields_registered?
|
81
|
+
handle_empty_arrays
|
82
|
+
@link_fields
|
83
|
+
end
|
84
|
+
|
85
|
+
# TODO: Refactor this to be less repetitive
|
86
|
+
def handle_empty_arrays
|
87
|
+
@fields = {}.with_indifferent_access if @fields.length == 0
|
88
|
+
@link_fields = {}.with_indifferent_access if @link_fields.length == 0
|
89
|
+
end
|
90
|
+
|
91
|
+
# Registers a single module by name
|
92
|
+
# Adds module to SugarCRM.modules (SugarCRM.modules << Module.new("Users"))
|
93
|
+
# Adds module class to SugarCRM parent module (SugarCRM.constants << User)
|
94
|
+
# Note, SugarCRM::User.module == Module.find("Users")
|
95
|
+
def register
|
96
|
+
return self if registered?
|
97
|
+
mod_instance = self
|
98
|
+
sess = @session
|
99
|
+
# class Class < SugarCRM::Base
|
100
|
+
# module_name = "Accounts"
|
101
|
+
# end
|
102
|
+
klass = Class.new(SugarCRM::Base) do
|
103
|
+
self._module = mod_instance
|
104
|
+
self.session = sess
|
105
|
+
end
|
106
|
+
|
107
|
+
# class Account < SugarCRM::Base
|
108
|
+
@session.namespace_const.const_set self.klass, klass
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
# Deregisters the module
|
113
|
+
def deregister
|
114
|
+
return true unless registered?
|
115
|
+
klass = self.klass
|
116
|
+
@session.namespace_const.instance_eval{ remove_const klass }
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
def registered?
|
121
|
+
@session.namespace_const.const_defined? @klass
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
@klass
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_class
|
129
|
+
SugarCRM.const_get(@klass).new
|
130
|
+
end
|
131
|
+
|
132
|
+
class << self
|
133
|
+
@initialized = false
|
134
|
+
|
135
|
+
# Registers all of the SugarCRM Modules
|
136
|
+
def register_all(session)
|
137
|
+
namespace = session.namespace_const
|
138
|
+
session.connection.get_modules.each do |m|
|
139
|
+
session.modules << m.register
|
140
|
+
end
|
141
|
+
@initialized = true
|
142
|
+
true
|
143
|
+
end
|
144
|
+
|
145
|
+
# Deregisters all of the SugarCRM Modules
|
146
|
+
def deregister_all(session)
|
147
|
+
namespace = session.namespace_const
|
148
|
+
session.modules.each do |m|
|
149
|
+
m.deregister
|
150
|
+
end
|
151
|
+
session.modules = []
|
152
|
+
@initialized = false
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
# Finds a module by name, or klass name
|
157
|
+
def find(name, session=nil)
|
158
|
+
session ||= SugarCRM.session
|
159
|
+
register_all(session) unless initialized?
|
160
|
+
session.modules.each do |m|
|
161
|
+
return m if m.name == name
|
162
|
+
return m if m.klass == name
|
163
|
+
end
|
164
|
+
false
|
165
|
+
end
|
166
|
+
|
167
|
+
# Class variable to track if we've initialized or not
|
168
|
+
def initialized?
|
169
|
+
@initialized ||= false
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|