sugarcrm_emp 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/.document +5 -0
  2. data/.gitignore +29 -0
  3. data/Gemfile +14 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +275 -0
  6. data/Rakefile +44 -0
  7. data/VERSION +1 -0
  8. data/WATCHLIST.rdoc +7 -0
  9. data/bin/sugarcrm +26 -0
  10. data/lib/rails/generators/sugarcrm/config/config_generator.rb +22 -0
  11. data/lib/rails/generators/sugarcrm/config/templates/initializer.rb +4 -0
  12. data/lib/rails/generators/sugarcrm/config/templates/sugarcrm.yml +19 -0
  13. data/lib/sugarcrm/associations/association.rb +170 -0
  14. data/lib/sugarcrm/associations/association_cache.rb +36 -0
  15. data/lib/sugarcrm/associations/association_collection.rb +141 -0
  16. data/lib/sugarcrm/associations/association_methods.rb +91 -0
  17. data/lib/sugarcrm/associations/associations.rb +61 -0
  18. data/lib/sugarcrm/associations.rb +5 -0
  19. data/lib/sugarcrm/attributes/attribute_methods.rb +203 -0
  20. data/lib/sugarcrm/attributes/attribute_serializers.rb +55 -0
  21. data/lib/sugarcrm/attributes/attribute_typecast.rb +44 -0
  22. data/lib/sugarcrm/attributes/attribute_validations.rb +62 -0
  23. data/lib/sugarcrm/attributes.rb +4 -0
  24. data/lib/sugarcrm/base.rb +355 -0
  25. data/lib/sugarcrm/config/sugarcrm.yaml +10 -0
  26. data/lib/sugarcrm/connection/api/get_available_modules.rb +22 -0
  27. data/lib/sugarcrm/connection/api/get_document_revision.rb +14 -0
  28. data/lib/sugarcrm/connection/api/get_entries.rb +23 -0
  29. data/lib/sugarcrm/connection/api/get_entries_count.rb +20 -0
  30. data/lib/sugarcrm/connection/api/get_entry.rb +23 -0
  31. data/lib/sugarcrm/connection/api/get_entry_list.rb +31 -0
  32. data/lib/sugarcrm/connection/api/get_module_fields.rb +15 -0
  33. data/lib/sugarcrm/connection/api/get_note_attachment.rb +14 -0
  34. data/lib/sugarcrm/connection/api/get_relationships.rb +30 -0
  35. data/lib/sugarcrm/connection/api/get_report_entries.rb +17 -0
  36. data/lib/sugarcrm/connection/api/get_server_info.rb +7 -0
  37. data/lib/sugarcrm/connection/api/get_user_id.rb +13 -0
  38. data/lib/sugarcrm/connection/api/get_user_team_id.rb +14 -0
  39. data/lib/sugarcrm/connection/api/login.rb +18 -0
  40. data/lib/sugarcrm/connection/api/logout.rb +15 -0
  41. data/lib/sugarcrm/connection/api/seamless_login.rb +13 -0
  42. data/lib/sugarcrm/connection/api/search_by_module.rb +25 -0
  43. data/lib/sugarcrm/connection/api/set_campaign_merge.rb +15 -0
  44. data/lib/sugarcrm/connection/api/set_document_revision.rb +35 -0
  45. data/lib/sugarcrm/connection/api/set_entries.rb +15 -0
  46. data/lib/sugarcrm/connection/api/set_entry.rb +15 -0
  47. data/lib/sugarcrm/connection/api/set_note_attachment.rb +25 -0
  48. data/lib/sugarcrm/connection/api/set_relationship.rb +27 -0
  49. data/lib/sugarcrm/connection/api/set_relationships.rb +22 -0
  50. data/lib/sugarcrm/connection/connection.rb +201 -0
  51. data/lib/sugarcrm/connection/helper.rb +50 -0
  52. data/lib/sugarcrm/connection/request.rb +61 -0
  53. data/lib/sugarcrm/connection/response.rb +91 -0
  54. data/lib/sugarcrm/connection.rb +5 -0
  55. data/lib/sugarcrm/connection_pool.rb +163 -0
  56. data/lib/sugarcrm/exceptions.rb +23 -0
  57. data/lib/sugarcrm/extensions/README.txt +23 -0
  58. data/lib/sugarcrm/finders/dynamic_finder_match.rb +41 -0
  59. data/lib/sugarcrm/finders/finder_methods.rb +243 -0
  60. data/lib/sugarcrm/finders.rb +2 -0
  61. data/lib/sugarcrm/module.rb +174 -0
  62. data/lib/sugarcrm/module_methods.rb +91 -0
  63. data/lib/sugarcrm/session.rb +218 -0
  64. data/lib/sugarcrm.rb +22 -0
  65. data/sugarcrm.gemspec +178 -0
  66. data/test/config_test.yaml +15 -0
  67. data/test/connection/test_get_available_modules.rb +9 -0
  68. data/test/connection/test_get_entries.rb +15 -0
  69. data/test/connection/test_get_entry.rb +22 -0
  70. data/test/connection/test_get_entry_list.rb +23 -0
  71. data/test/connection/test_get_module_fields.rb +11 -0
  72. data/test/connection/test_get_relationships.rb +12 -0
  73. data/test/connection/test_get_server_info.rb +9 -0
  74. data/test/connection/test_get_user_id.rb +9 -0
  75. data/test/connection/test_get_user_team_id.rb +9 -0
  76. data/test/connection/test_login.rb +9 -0
  77. data/test/connection/test_logout.rb +9 -0
  78. data/test/connection/test_set_document_revision.rb +28 -0
  79. data/test/connection/test_set_entry.rb +15 -0
  80. data/test/connection/test_set_note_attachment.rb +16 -0
  81. data/test/connection/test_set_relationship.rb +18 -0
  82. data/test/extensions_test/patch.rb +9 -0
  83. data/test/helper.rb +17 -0
  84. data/test/test_association_collection.rb +11 -0
  85. data/test/test_associations.rb +156 -0
  86. data/test/test_connection.rb +13 -0
  87. data/test/test_connection_pool.rb +40 -0
  88. data/test/test_finders.rb +201 -0
  89. data/test/test_module.rb +51 -0
  90. data/test/test_request.rb +35 -0
  91. data/test/test_response.rb +26 -0
  92. data/test/test_session.rb +136 -0
  93. data/test/test_sugarcrm.rb +213 -0
  94. 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,2 @@
1
+ require 'sugarcrm/finders/finder_methods'
2
+ require 'sugarcrm/finders/dynamic_finder_match'
@@ -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