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