sugarcrm_emp 0.10.0
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/.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
|