sugarcrm 0.9.8 → 0.9.9

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/Gemfile CHANGED
@@ -2,6 +2,7 @@ source "http://rubygems.org"
2
2
 
3
3
  gem "activesupport", ">= 3.0.0", :require => "active_support"
4
4
  gem "i18n"
5
+ gem "json"
5
6
 
6
7
  # Add dependencies to develop your gem here.
7
8
  # Include everything needed to run rake, tests, features, etc.
@@ -9,5 +10,4 @@ group :development do
9
10
  gem "shoulda", ">= 0"
10
11
  gem "bundler", "~> 1.0.0"
11
12
  gem "jeweler", "~> 1.5.2"
12
- gem "rcov", ">= 0"
13
13
  end
data/README.rdoc CHANGED
@@ -143,6 +143,46 @@ Instead of SugarCRM.connection.get_entry("Users", "1") you could use SugarCRM::U
143
143
  }
144
144
  )
145
145
 
146
+ == USING A CONFIGURATION FILE
147
+
148
+ If you want to use a configuration file instead of always specifying the url, username, and password to connect to SugarCRM, you can either
149
+
150
+ * add your credentials to `/etc/sugarcrm.yaml`
151
+ * add your credentials to `~/.sugarcrm.yaml`
152
+ * add your credentials to `config/sugarcrm.yaml` (will need to be copied each time you upgrade or reinstall the gem)
153
+ * add your credentials to a YAML file and call `SugarCRM::Environment.load_config` followed by the absolute path to your configuration file
154
+
155
+ If there are several configuration files, they are loaded sequentially in the order above and will overwrite previous values (if present). This allows you to (e.g.) have a config file in `/etc/sugarcrm.yaml` with system-wide configuration information (such as the url where SugarCRM is located) and/or defaults. Each developer/user can then have his personal configuration file in `~/.sugarcrm.yaml` with his own username and password. A developer could also specify a different location for the SugarCRM instance (e.g. a local testing instance) in his configuration file, which will take precedence over the value in `/etc/sugarcrm.yaml`.
156
+
157
+ Your configuration should be in YAML format:
158
+
159
+ config:
160
+ base_url: http://127.0.0.1/sugarcrm
161
+ username: admin
162
+ password: letmein
163
+
164
+ An example, accompanied by instructions, can be found in the `config/sugarcrm.yaml` file. In addition, a working example used for testing can be found in `test/config_test.yaml`
165
+
166
+ == USING THE GEM IN A CONSOLE
167
+
168
+ 1. Type `irb` in your command prompt
169
+
170
+ 2. Require the gem with `require 'sugarcrm'`
171
+
172
+ 3. * if your login credentials are stored in the `config/sugarcrm.yaml` file, you have been automagically logged in already ;
173
+ * if your login credentials are stored in a different config file, just call `SugarCRM::Environment.load_config` followed by the absolute path to your config file. This will log you in automatically ;
174
+ * if you don't have a configuration file, you can still call the basic `SugarCRM.connect` and give it the proper arguments (see documentation above)
175
+
176
+ 4. You now have full access to the gem's functionality, e.g. `puts SugarCRM::Account.first.name`
177
+
178
+ == EXPANDING THE GEM
179
+
180
+ If you want to expand the gem's capabilities (e.g. to add methods specific to your environment), you can either
181
+
182
+ * drop your `*.rb` files in `lib/sugarcrm/extensions/` (see the README in that folder)
183
+
184
+ * drop your `*.rb` files in any other folder and call `SugarCRM::Environment.extensions_folder = ` followed by the absolute path to the folder containing your extensions
185
+
146
186
  == REQUIREMENTS:
147
187
 
148
188
  * >= activesupport 3.0.0 gem
@@ -151,6 +191,10 @@ Instead of SugarCRM.connection.get_entry("Users", "1") you could use SugarCRM::U
151
191
 
152
192
  * sudo gem install sugarcrm
153
193
 
194
+ == TEST:
195
+
196
+ Set the values in `test/helper.rb` to point to a SugarCRM instance with demo data
197
+
154
198
  == Note on Patches/Pull Requests
155
199
 
156
200
  * Fork the project.
data/Rakefile CHANGED
@@ -28,12 +28,12 @@ Rake::TestTask.new(:test) do |test|
28
28
  test.verbose = true
29
29
  end
30
30
 
31
- require 'rcov/rcovtask'
32
- Rcov::RcovTask.new do |test|
33
- test.libs << 'test'
34
- test.pattern = 'test/**/test_*.rb'
35
- test.verbose = true
36
- end
31
+ #require 'rcov/rcovtask'
32
+ #Rcov::RcovTask.new do |test|
33
+ # test.libs << 'test'
34
+ # test.pattern = 'test/**/test_*.rb'
35
+ # test.verbose = true
36
+ #end
37
37
 
38
38
  task :default => :test
39
39
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.8
1
+ 0.9.9
data/lib/sugarcrm.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  require 'net/https'
2
2
  require 'pp'
3
3
  require 'set'
4
+ require 'cgi'
4
5
  require 'uri'
5
6
  require 'rubygems'
6
7
  require 'active_support/core_ext'
8
+ require 'json'
7
9
 
10
+ require 'sugarcrm/environment'
8
11
  require 'sugarcrm/module_methods'
9
12
  require 'sugarcrm/connection'
10
13
  require 'sugarcrm/exceptions'
@@ -1,12 +1,13 @@
1
1
  module SugarCRM
2
2
  # Represents an association and it's metadata
3
3
  class Association
4
- attr :owner
5
- attr :target
6
- attr :link_field
7
- attr :attributes
8
- attr :methods
4
+ attr :owner, true
5
+ attr :target, true
6
+ attr :link_field, true
7
+ attr :attributes, true
8
+ attr :methods, true
9
9
 
10
+ # TODO: Describe this.
10
11
  def initialize(owner,link_field,opts={})
11
12
  @options = { :define_methods? => true }.merge! opts
12
13
  @owner = owner
@@ -35,11 +36,11 @@ module SugarCRM
35
36
  end
36
37
 
37
38
  # Attempts to determine the class of the target in the association
39
+ # TODO: Write tests for this.
38
40
  def resolve_target
39
41
  # Use the link_field name first
40
42
  klass = @link_field.singularize.camelize
41
43
  return "SugarCRM::#{klass}".constantize if SugarCRM.const_defined? klass
42
-
43
44
  # Use the link_field attribute "module"
44
45
  if @attributes["module"].length > 0
45
46
  module_name = SugarCRM::Module.find(@attributes["module"])
@@ -54,14 +55,12 @@ module SugarCRM
54
55
  end
55
56
 
56
57
  # Generates the association proxy method for related module
57
- def define_method(link_field, pretty_name=nil)
58
- pretty_name ||= link_field
58
+ def define_method(link_field)
59
59
  @owner.class.module_eval %Q?
60
- def #{pretty_name}
60
+ def #{link_field}
61
61
  query_association :#{link_field}
62
62
  end
63
63
  ?
64
- pretty_name
65
64
  end
66
65
 
67
66
  # Defines methods for accessing the association target on the owner class.
@@ -71,10 +70,10 @@ module SugarCRM
71
70
  def define_methods
72
71
  methods = []
73
72
  pretty_name = humanized_link_name(@link_field)
74
- methods << define_method(pretty_name)
73
+ methods << define_method(@link_field)
75
74
  if pretty_name != @link_field
76
75
  @owner.class.module_eval %Q?
77
- alias :#{@link_field} #{pretty_name}
76
+ alias :#{pretty_name} #{@link_field}
78
77
  ?
79
78
  methods << @link_field
80
79
  end
@@ -85,18 +84,9 @@ module SugarCRM
85
84
  # e.g. if a custom relationship is defined in Studio between Tasks and Documents,
86
85
  # the link_field will be `tasks_documents` but a human would call the relationship `documents`
87
86
  def humanized_link_name(link_field)
88
- # Split the relationship name into parts
89
- # "contact_accounts" => ["contact","accounts"]
90
- m = link_field.split(/_/)
91
- # Determine the parts we don't want
92
- # SugarCRM::Contact => ["contacts", "contact"]
93
- o = @owner.class._module.table_name
94
- # Use array subtraction to remove parts representing the owner side of the relationship
95
- # ["contact", "accounts"] - ["contacts", "contact"] => ["accounts"]
96
- t = m - [o, o.singularize]
97
- # Reassemble whatever's left
98
- # "accounts"
99
- t.join('_')
87
+ # the module name is used to function properly with modules containing '_' (e.g. a custom module abc_sale : custom modules need a prefix (abc here) so they will always have a '_' in their table name)
88
+ return link_field unless link_field.to_s =~ /((.*)_)?#{Regexp.quote(@owner.class._module.name.downcase)}(_(.*))?/
89
+ [$2, $4].compact.join('_')
100
90
  end
101
91
 
102
92
  end
@@ -7,7 +7,17 @@ module SugarCRM; module AssociationCache
7
7
  @association_cache.symbolize_keys.include? association.to_sym
8
8
  end
9
9
 
10
- protected
10
+ # Updates an association cache entry if it's been initialized
11
+ def update_association_cache_for(association, target, action=:add)
12
+ return unless association_cached? association
13
+ case action
14
+ when :add
15
+ return if @association_cache[association].collection.include? target
16
+ @association_cache[association].push(target) # don't use `<<` because overriden method in AssociationCollection gets called instead
17
+ when :delete
18
+ @association_cache[association].delete target
19
+ end
20
+ end
11
21
 
12
22
  # Returns true if an association collection has changed
13
23
  def associations_changed?
@@ -17,11 +27,7 @@ module SugarCRM; module AssociationCache
17
27
  false
18
28
  end
19
29
 
20
- # Updates an association cache entry if it's been initialized
21
- def update_association_cache_for(association, target)
22
- # only add to the cache if the relationship has been queried
23
- @association_cache[association] << target if association_cached? association
24
- end
30
+ protected
25
31
 
26
32
  # Resets the association cache
27
33
  def clear_association_cache
@@ -2,6 +2,8 @@ module SugarCRM
2
2
  # A class for handling association collections. Basically just an extension of Array
3
3
  # doesn't actually load the records from Sugar until you invoke one of the public methods
4
4
  class AssociationCollection
5
+
6
+ attr_reader :collection
5
7
 
6
8
  # creates a new instance of an AssociationCollection
7
9
  # Owner is the parent object, and association is the target
@@ -66,7 +68,8 @@ module SugarCRM
66
68
  record.save! if record.new?
67
69
  result = true
68
70
  result = false if include?(record)
69
- @collection << record
71
+ @owner.update_association_cache_for(@association, record, :add)
72
+ record.update_association_cache_for(record.associations.find!(@owner).link_field, @owner, :add)
70
73
  result && self
71
74
  end
72
75
  alias :add :<<
@@ -24,7 +24,7 @@ module SugarCRM; module AssociationMethods
24
24
  # In contrast to using account.contacts << contact, this method doesn't load the relationships
25
25
  # before setting the new relationship.
26
26
  # This method is useful when certain modules have many links to other modules: not loading the
27
- # relationships allows one ot avoid a Timeout::Error
27
+ # relationships allows one to avoid a Timeout::Error
28
28
  def associate!(target,opts={})
29
29
  targets = Array.wrap(target)
30
30
  targets.each do |t|
@@ -37,10 +37,25 @@ module SugarCRM; module AssociationMethods
37
37
  raise AssociationFailed,
38
38
  "Couldn't associate #{self.class._module.name}: #{self.id} -> #{t}: #{t.id}!"
39
39
  end
40
- update_association_cache_for(association.link_field, t)
40
+ # We need to update the association cache for any changes we make.
41
+ if opts[:delete]
42
+ update_association_cache_for(association.link_field, t, :delete)
43
+ t.update_association_cache_for(association.link_field, self, :delete)
44
+ else
45
+ update_association_cache_for(association.link_field, t, :add)
46
+ t.update_association_cache_for(t.associations.find!(self).link_field, self, :add)
47
+ end
41
48
  end
42
49
  true
43
50
  end
51
+ alias :relate! :associate!
52
+
53
+ # Removes a relationship between the current object and the target object
54
+ # TODO: Write a test for this.
55
+ def disassociate!(target)
56
+ associate!(target,{:delete => 1})
57
+ end
58
+ alias :unrelate! :disassociate!
44
59
 
45
60
  protected
46
61
 
data/lib/sugarcrm/base.rb CHANGED
@@ -27,7 +27,7 @@ module SugarCRM; class Base
27
27
  def establish_connection(url, user, pass, opts={})
28
28
  options = {
29
29
  :debug => false,
30
- :register_modules => true,
30
+ :register_modules => true
31
31
  }.merge(opts)
32
32
  @debug = options[:debug]
33
33
  @@connection = SugarCRM::Connection.new(url, user, pass, options)
@@ -96,7 +96,9 @@ module SugarCRM; class Base
96
96
 
97
97
  def find_initial(options)
98
98
  options.update(:limit => 1)
99
- find_every(options)
99
+ result = find_by_sql(options)
100
+ return result.first if result.instance_of? Array # find_by_sql will return an Array if result are found
101
+ result
100
102
  end
101
103
 
102
104
  def find_from_ids(ids, options)
@@ -152,8 +154,56 @@ module SugarCRM; class Base
152
154
  end
153
155
 
154
156
  def find_by_sql(options)
155
- query = query_from_options(options)
156
- SugarCRM.connection.get_entry_list(self._module.name, query, options) || nil # return nil instead of false if no results are found
157
+ # SugarCRM REST API has a bug where, when :limit and :offset options are passed simultaneously, :limit is considered to be the smallest of the two, and :offset is the larger
158
+ # in addition to allowing querying of large datasets while avoiding timeouts,
159
+ # this implementation fixes the :limit - :offset bug so that it behaves correctly
160
+ local_options = {}
161
+ options.keys.each{|k|
162
+ local_options[k] = options[k]
163
+ }
164
+ local_options.delete(:offset) if local_options[:offset] == 0
165
+
166
+ # store the number of records wanted by user, as we'll overwrite :limit option to obtain several slices of records (to avoid timeout issues)
167
+ nb_to_fetch = local_options[:limit]
168
+ nb_to_fetch = nb_to_fetch.to_i if nb_to_fetch
169
+ offset_value = local_options[:offset] || 10 # arbitrary value, must be bigger than :limit used (see comment above)
170
+ offset_value = offset_value.to_i
171
+ offset_value.freeze
172
+ initial_limit = nb_to_fetch.nil? ? offset_value : [offset_value, nb_to_fetch].min # how many records should be fetched on first pass
173
+ # ensure results are ordered so :limit and :offset option behave in a deterministic fashion
174
+ local_options = { :order_by => :id }.merge(local_options)
175
+ local_options.update(:limit => initial_limit) # override original argument
176
+
177
+ # get first slice of results
178
+ # note: to work around a SugarCRM REST API bug, the :limit option must always be smaller than the :offset option
179
+ # this is the reason this first query is separate (not in the loop): the initial query has a larger limit, so that we can then use the loop
180
+ # with :limit always smaller than :offset
181
+ results = SugarCRM.connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
182
+ return nil unless results
183
+ results = Array.wrap(results)
184
+
185
+ limit_value = [5, offset_value].min # arbitrary value, must be smaller than :offset used (see comment above)
186
+ limit_value.freeze
187
+ local_options = { :order_by => :id }.merge(local_options)
188
+ local_options.update(:limit => limit_value)
189
+
190
+ # a portion of the results has already been queried
191
+ # update or set the :offset value to reflect this
192
+ local_options[:offset] ||= results.size
193
+ local_options[:offset] += offset_value
194
+
195
+ # continue fetching results until we either
196
+ # a) have as many results as the user wants (specified via the original :limit option)
197
+ # b) there are no more results matching the criteria
198
+ while result_slice = SugarCRM.connection.get_entry_list(self._module.name, query_from_options(local_options), local_options)
199
+ results.concat(Array.wrap(result_slice))
200
+ # make sure we don't return more results than the user requested (via original :limit option)
201
+ if nb_to_fetch && results.size >= nb_to_fetch
202
+ return results.slice(0, nb_to_fetch)
203
+ end
204
+ local_options[:offset] += local_options[:limit] # update :offset as we get more records
205
+ end
206
+ results
157
207
  end
158
208
 
159
209
  def query_from_options(options)
@@ -0,0 +1,10 @@
1
+ # below is an example configuration file
2
+ #
3
+ # to create you own configuration file, simply copy and adapt the text below, removing the '#' in front of the key-value pairs
4
+ #
5
+ # you'll find an example of a configuration file (used for tests) in test/config_test.yaml
6
+ #
7
+ # config:
8
+ # base_url: http://127.0.0.1/sugarcrm # where your SugarCRM instance is located
9
+ # username: admin
10
+ # password: letmein
@@ -10,6 +10,5 @@ module SugarCRM; class Connection
10
10
  EOF
11
11
  json.gsub!(/^\s{6}/,'')
12
12
  SugarCRM::Response.handle(send!(:get_document_revision, json))
13
- #send!(:get_document_revision, json)
14
13
  end
15
14
  end; end
@@ -14,7 +14,7 @@ module SugarCRM; class Connection
14
14
  \"module_name\": \"#{module_name}\"\,
15
15
  \"ids\": #{ids.to_json}\,
16
16
  \"select_fields\": #{resolve_fields(module_name, options[:fields])}\,
17
- \"link_name_to_fields_array\": #{options[:link_fields].to_json}\,
17
+ \"link_name_to_fields_array\": #{options[:link_fields].to_json}
18
18
  }
19
19
  EOF
20
20
  json.gsub!(/^\s{6}/,'')
@@ -13,7 +13,7 @@ module SugarCRM; class Connection
13
13
  \"module_name\": \"#{module_name}\"\,
14
14
  \"id\": \"#{id}\"\,
15
15
  \"select_fields\": #{resolve_fields(module_name, options[:fields])}\,
16
- \"link_name_to_fields_array\": #{options[:link_fields]}\,
16
+ \"link_name_to_fields_array\": #{options[:link_fields]}
17
17
  }
18
18
  EOF
19
19
 
@@ -1,15 +1,15 @@
1
1
  module SugarCRM; class Connection
2
2
  # Logs the user into the Sugar application.
3
3
  def login
4
- connect! unless connected?
4
+ connect! unless connected?
5
5
  json = <<-EOF
6
6
  {
7
- \"user_auth\": {
8
- \"user_name\": \"#{@user}\"\,
9
- \"password\": \"#{OpenSSL::Digest::MD5.new(@pass)}\"\,
10
- \"version\": \"2\"\,
7
+ "user_auth": {
8
+ "user_name": "#{@user}",
9
+ "password": "#{OpenSSL::Digest::MD5.new(@pass)}",
10
+ "version": 2
11
11
  },
12
- \"application\": \"\"
12
+ "application": "sugarcrm_rubygem"
13
13
  }
14
14
  EOF
15
15
  json.gsub!(/^\s{6}/,'')
@@ -18,7 +18,8 @@ module SugarCRM; class Connection
18
18
  def initialize(url, user, pass, options={})
19
19
  @options = {
20
20
  :debug => false,
21
- :register_modules => true
21
+ :register_modules => true,
22
+ :load_environment => true
22
23
  }.merge(options)
23
24
  @url = URI.parse(url)
24
25
  @user = user
@@ -27,6 +28,8 @@ module SugarCRM; class Connection
27
28
  @response = ""
28
29
  resolve_url
29
30
  login!
31
+ # make sure the environment singleton gets loaded
32
+ SugarCRM::Environment.instance if @options[:load_environment]
30
33
  self
31
34
  end
32
35
 
@@ -123,12 +126,12 @@ module SugarCRM; class Connection
123
126
  return @response.body if RESPONSE_IS_NOT_JSON.include? @request.method
124
127
  begin
125
128
  # Push it through the old meat grinder.
126
- response_json = ActiveSupport::JSON.decode(@response.body)
129
+ response_json = JSON.parse(@response.body)
127
130
  rescue StandardError => e
128
131
  raise UnhandledResponse, @response.body
129
132
  end
130
133
  # Empty result. Is this wise?
131
- return false if response_json["result_count"] == 0
134
+ return nil if response_json["result_count"] == 0
132
135
  # Filter debugging on REALLY BIG responses
133
136
  if @options[:debug] && !(DONT_SHOW_DEBUG_FOR.include? @request.method)
134
137
  puts "#{@request.method}: JSON Response:"
@@ -8,14 +8,14 @@ module SugarCRM; class Request
8
8
  def initialize(url, method, json, debug=false)
9
9
  @url = url
10
10
  @method = method
11
- @json = json
11
+ @json = CGI.escape(json)
12
12
  @request = 'method=' << @method.to_s
13
13
  @request << '&input_type=JSON'
14
14
  @request << '&response_type=JSON'
15
15
  @request << '&rest_data=' << @json
16
16
  if debug
17
17
  puts "#{method}: Request:"
18
- puts @request
18
+ puts json
19
19
  puts "\n"
20
20
  end
21
21
  self
@@ -26,6 +26,6 @@ module SugarCRM; class Request
26
26
  end
27
27
 
28
28
  def to_s
29
- URI.escape(@request)
29
+ @request
30
30
  end
31
31
  end; end
@@ -17,9 +17,8 @@ module SugarCRM; class Response
17
17
  if SugarCRM.connection.debug?
18
18
  puts "Failed to process JSON:"
19
19
  pp json
20
- raise e
21
20
  end
22
- return json
21
+ raise e
23
22
  end
24
23
  end
25
24
  end
@@ -36,7 +35,7 @@ module SugarCRM; class Response
36
35
  # populated from the response
37
36
  def to_obj
38
37
  # If this is not a "entry_list" response, just return
39
- return @response unless @response["entry_list"]
38
+ return @response unless @response && @response["entry_list"]
40
39
 
41
40
  objects = []
42
41
  @response["entry_list"].each do |object|
@@ -0,0 +1,48 @@
1
+ require 'singleton'
2
+
3
+ module SugarCRM; class Environment
4
+ include Singleton
5
+
6
+ attr_reader :config
7
+
8
+ def initialize
9
+ @config = {}
10
+
11
+ # see README for reasoning behind the priorization
12
+ ['/etc/sugarcrm.yaml', File.expand_path('~/.sugarcrm.yaml'), File.join(File.dirname(__FILE__), 'config', 'sugarcrm.yaml')].each{|path|
13
+ load_config path if File.exists? path
14
+ }
15
+ extensions_folder = File.join(File.dirname(__FILE__), 'extensions')
16
+ SugarCRM::Base.establish_connection(@config[:base_url], @config[:username], @config[:password], {:load_environment => false}) if SugarCRM.connection.nil? && connection_info_loaded?
17
+ end
18
+
19
+ def connection_info_loaded?
20
+ @config[:base_url] && @config[:username] && @config[:password]
21
+ end
22
+
23
+ def load_config(path)
24
+ validate_path path
25
+ config = YAML.load_file(path)
26
+ if config && config["config"]
27
+ config["config"].each{|k,v|
28
+ @config[k.to_sym] = v
29
+ }
30
+ end
31
+ end
32
+
33
+ # load all the monkey patch extension files in the provided folder
34
+ def extensions_folder=(folder, dirstring=nil)
35
+ validate_path folder
36
+ path = File.expand_path(folder, dirstring)
37
+ Dir[File.join(path, '**', '*.rb').to_s].each { |f| load(f) }
38
+ end
39
+
40
+ def self.method_missing(method_id, *args, &block)
41
+ self.instance.send(method_id, *args, &block)
42
+ end
43
+
44
+ private
45
+ def validate_path(path)
46
+ raise "Invalid path: #{path}" unless File.exists? path
47
+ end
48
+ end; 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.find(: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.
@@ -15,7 +15,11 @@ module SugarCRM
15
15
  def initialize(name)
16
16
  @name = name
17
17
  @klass = name.classify
18
- @table_name = name.tableize
18
+ unless custom_module?
19
+ @table_name = name.tableize
20
+ else
21
+ @table_name = @name
22
+ end
19
23
  @custom_table_name = resolve_custom_table_name
20
24
  @fields = {}
21
25
  @link_fields = {}
@@ -36,8 +40,11 @@ module SugarCRM
36
40
  # For custom modules (created in the Studio), table name don't need to be tableized since
37
41
  # the name passed to the constructor is already tableized
38
42
  def resolve_custom_table_name
39
- @custom_table_name = @table_name + "_cstm"
40
- @custom_table_name = @name + "_cstm" if custom_module?
43
+ if custom_module?
44
+ @custom_table_name = @name + "_cstm"
45
+ else
46
+ @custom_table_name = @table_name + "_cstm"
47
+ end
41
48
  end
42
49
 
43
50
  # Returns the fields associated with the module
@@ -7,7 +7,7 @@ module SugarCRM
7
7
  def self.connection=(connection)
8
8
  @@connection = connection
9
9
  end
10
- def self.connect(url, user, pass, options={})
10
+ def self.connect(url=SugarCRM::Environment.config[:base_url], user=SugarCRM::Environment.config[:username], pass=SugarCRM::Environment.config[:password], options={})
11
11
  SugarCRM::Base.establish_connection(url, user, pass, options)
12
12
  end
13
13
  class << self
@@ -26,4 +26,21 @@ module SugarCRM
26
26
  SugarCRM::User.find_by_user_name(connection.user)
27
27
  end
28
28
 
29
+ # If a user tries to access a SugarCRM class before they're logged in,
30
+ # try to log in using credentials from config file.
31
+ # This will trigger module loading,
32
+ # and we can then attempt to return the requested class automagically
33
+ def self.const_missing(sym)
34
+ # if we're logged in, modules should be loaded and available
35
+ if SugarCRM.connection && SugarCRM.connection.logged_in?
36
+ super
37
+ else
38
+ # here, we initialize the environment (which happens on any method call, if singleton hasn't already been initialized)
39
+ # initializing the environment will log user in if credentials present in config file
40
+ # if it isn't possible to log in and access modules, pass the exception on
41
+ super unless SugarCRM::Environment.connection_info_loaded?
42
+ # try and return the requested module
43
+ SugarCRM.const_get(sym)
44
+ end
45
+ end
29
46
  end
@@ -0,0 +1,15 @@
1
+ # below is an example configuration file
2
+ #
3
+ # to create you own configuration file, simply copy and adapt the text below, removing the '#' in front of the key-value pairs
4
+ #
5
+ # you'll find an example of a configuration file (used for tests) in test/config_test.yaml
6
+ #
7
+ # config:
8
+ # base_url: http://127.0.0.1/sugarcrm # where your SugarCRM instance is located
9
+ # username: admin
10
+ # password: letmein
11
+
12
+ config:
13
+ base_url: http://127.0.0.1/sugarcrm # where your SugarCRM instance is located
14
+ username: admin
15
+ password: letmein
@@ -0,0 +1,9 @@
1
+ SugarCRM::Contact.class_eval do
2
+ def self.is_extended?
3
+ true
4
+ end
5
+
6
+ def is_extended?
7
+ true
8
+ end
9
+ end
@@ -1 +1,18 @@
1
- #TODO: Figure out how to test this.
1
+ #TODO: Figure out how to test this.
2
+
3
+
4
+ # That code works, but what didn't work:
5
+ # document.tasks.size
6
+ # document.associate!(task)
7
+ # document.tasks.size
8
+ # This seems to be fixed in HEAD in my repo. I haven't pushed the changes to your repo, because I don't really see why commit https://github.com/davidsulc/sugarcrm/commit/228375348c9113324370afa0aca4120eb117d3e1 fixes the issue...
9
+ # Also, I fixed this case
10
+ # document.tasks.size
11
+ # document.associate!(task)
12
+ # document.tasks.size # => 1 (correct)
13
+ # document.associate!(task)
14
+ # document.tasks.size # => 2 (incorrect: should remain 1)
15
+ # One thing we should look into is
16
+ # document.associate!(task)
17
+ # document.tasks.size # => 1
18
+ # task.documents.size # => 0 (should be 1)
@@ -0,0 +1,38 @@
1
+ require 'helper'
2
+
3
+ class TestEnvironment < Test::Unit::TestCase
4
+ context "A SugarCRM::Environment singleton" do
5
+
6
+ should "delegate missing methods to singleton instance" do
7
+ assert_equal SugarCRM::Environment.instance.config, SugarCRM::Environment.config
8
+ end
9
+
10
+ should "load monkey patch extensions" do
11
+ SugarCRM::Environment.extensions_folder = File.join(File.dirname(__FILE__), 'extensions_test')
12
+ assert SugarCRM::Contact.is_extended?
13
+ assert SugarCRM::Contact.is_extended?
14
+ end
15
+
16
+ should "load config file" do
17
+ SugarCRM::Environment.load_config File.join(File.dirname(__FILE__), 'config_test.yaml')
18
+
19
+ config_contents = {
20
+ :config => {
21
+ :base_url => 'http://127.0.0.1/sugarcrm',
22
+ :username => 'admin',
23
+ :password => 'letmein'
24
+ }
25
+ }
26
+
27
+ config_contents[:config].each{|k,v|
28
+ assert_equal v, SugarCRM::Environment.config[k]
29
+ }
30
+ end
31
+
32
+ should "log in to Sugar automatically if credentials are present in config file" do
33
+ SugarCRM::Environment.load_config File.join(File.dirname(__FILE__), 'config_test.yaml')
34
+ assert SugarCRM.connection.logged_in?
35
+ end
36
+ end
37
+
38
+ end
@@ -52,28 +52,26 @@ class TestSugarCRM < Test::Unit::TestCase
52
52
  end
53
53
 
54
54
  should "not save a record that is missing required attributes" do
55
- SugarCRM.connection.debug = false
56
55
  u = SugarCRM::User.new
57
56
  u.last_name = "Test"
58
57
  assert !u.save
59
- SugarCRM.connection.debug = false
60
58
  assert_raise SugarCRM::InvalidRecord do
61
59
  u.save!
62
60
  end
63
61
  end
64
62
 
65
63
  should "always return an Array when :all" do
66
- users = SugarCRM::User.all
64
+ users = SugarCRM::User.all(:limit => 10)
67
65
  assert_instance_of Array, users
68
66
  users = SugarCRM::User.find(:all, :conditions => {:user_name => '= admin'})
69
67
  assert_instance_of Array, users
68
+ assert users.length == 1
70
69
  users = SugarCRM::User.find(:all, :conditions => {:user_name => '= invalid_user_123'})
71
70
  assert_instance_of Array, users
72
71
  assert users.length == 0
73
72
  end
74
73
 
75
74
  should "create, modify, and delete a record" do
76
- #SugarCRM.connection.debug = true
77
75
  u = SugarCRM::User.new
78
76
  u.email1 = "abc@abc.com"
79
77
  u.first_name = "Test"
@@ -85,10 +83,10 @@ class TestSugarCRM < Test::Unit::TestCase
85
83
  assert u.save!
86
84
  assert !u.new?
87
85
  m = SugarCRM::User.find_by_first_name_and_last_name("Test", "User")
86
+ assert m.user_name != "admin"
88
87
  m.title = "Test User"
89
88
  assert m.save!
90
89
  assert m.delete
91
- #SugarCRM.connection.debug = false
92
90
  end
93
91
 
94
92
  should "support finding first instance (sorted by attribute)" do
@@ -126,7 +124,48 @@ class TestSugarCRM < Test::Unit::TestCase
126
124
 
127
125
  should "return an array of records when sent #find([id1, id2, id3])" do
128
126
  users = SugarCRM::User.find(["seed_sarah_id", 1])
129
- assert_equal "Administrator", users.last.title
127
+ assert_equal "admin", users.last.user_name
128
+ end
129
+
130
+ # test Base#find_by_sql edge case
131
+ should "return an array of records with small limit and an offset of 0" do
132
+ accounts = SugarCRM::Account.all(:limit => 3, :offset => 0)
133
+ assert_equal 3, accounts.size
134
+ end
135
+
136
+ # test Base#find_by_sql standard case
137
+ should "return an array of records with high limit" do
138
+ accounts = SugarCRM::Account.all(:limit => 12)
139
+ assert_equal 12, accounts.size
140
+ end
141
+
142
+ should "return an array of records when using :order_by, :limit, and :offset options" do
143
+ accounts = SugarCRM::Account.all(:order_by => 'name', :limit => 3, :offset => 10)
144
+ accounts_api = SugarCRM.connection.get_entry_list('Accounts', '1=1', :order_by => 'name', :limit => 3, :offset => 10)
145
+ assert_equal accounts_api, accounts
146
+ end
147
+
148
+ should "return an array of records working around a SugarCRM bug when :limit > :offset" do
149
+ accounts = SugarCRM::Account.all(:order_by => 'name', :limit => 10, :offset => 2)
150
+ assert_equal 10, accounts.size
151
+ end
152
+
153
+ should "return an array of 1 record with :limit => 1, :offset => 1" do
154
+ accounts = SugarCRM::Account.all(:order_by => 'name', :limit => 1, :offset => 1)
155
+ assert_equal 1, accounts.size
156
+ end
157
+
158
+ should "ignore :offset => 0" do
159
+ accounts = SugarCRM::Account.all(:order_by => 'name', :limit => 3)
160
+ accounts_offset = SugarCRM::Account.all(:order_by => 'name', :limit => 3, :offset => 0)
161
+ assert_equal accounts, accounts_offset
162
+ end
163
+
164
+ should "compute offsets correctly" do
165
+ accounts = SugarCRM::Account.all(:order_by => 'name', :limit => 10, :offset => 3)
166
+ accounts_first_slice = SugarCRM::Account.all(:order_by => 'name', :limit => 5, :offset => 3)
167
+ accounts_second_slice = SugarCRM::Account.all(:order_by => 'name', :limit => 5, :offset => 8)
168
+ assert_equal accounts, accounts_first_slice.concat(accounts_second_slice)
130
169
  end
131
170
 
132
171
  should "return an instance of User when sent User#find_by_username" do
@@ -143,12 +182,64 @@ class TestSugarCRM < Test::Unit::TestCase
143
182
  assert a.delete
144
183
  end
145
184
 
146
- # should "support saving of records with special characters in them" do
147
- # a = SugarCRM::Account.new
148
- # a.name = "COHEN, WEISS & SIMON LLP"
149
- # assert a.save!
150
- # end
151
-
185
+ should "update association cache on associate! only if association changes" do
186
+ a = SugarCRM::Account.first
187
+ c = SugarCRM::Contact.create(:last_name => 'Doe')
188
+
189
+ nb_contacts = a.contacts.size
190
+ a.associate!(c)
191
+ assert_equal nb_contacts + 1, a.contacts.size
192
+ a.associate!(c)
193
+ assert_equal nb_contacts + 1, a.contacts.size # should not change: already associated
194
+
195
+ c.delete
196
+ end
197
+
198
+ should "update association cache on << only if association changes" do
199
+ a = SugarCRM::Account.first
200
+ c = SugarCRM::Contact.create(:last_name => 'Doe')
201
+
202
+ nb_contacts = a.contacts.size
203
+ a.contacts << c
204
+ assert_equal nb_contacts + 1, a.contacts.size
205
+ a.contacts << c
206
+ assert_equal nb_contacts + 1, a.contacts.size # should not change: already associated
207
+
208
+ c.delete
209
+ end
210
+
211
+ should "update association cache for both sides of the relationship when calling associate!" do
212
+ a = SugarCRM::Account.first
213
+ c = SugarCRM::Contact.create(:last_name => 'Doe')
214
+
215
+ nb_contacts = a.contacts.size
216
+ nb_accounts = c.accounts.size
217
+ a.associate!(c)
218
+ assert_equal nb_contacts + 1, a.contacts.size
219
+ assert_equal nb_accounts + 1, c.accounts.size
220
+
221
+ c.delete
222
+ end
223
+
224
+ should "update association cache for both sides of the relationship when calling <<" do
225
+ a = SugarCRM::Account.first
226
+ c = SugarCRM::Contact.create(:last_name => 'Doe')
227
+
228
+ nb_contacts = a.contacts.size
229
+ nb_accounts = c.accounts.size
230
+ a.contacts << c
231
+ assert_equal nb_contacts + 1, a.contacts.size
232
+ assert_equal nb_accounts + 1, c.accounts.size
233
+
234
+ c.delete
235
+ end
236
+
237
+ should "support saving of records with special characters in them" do
238
+ a = SugarCRM::Account.new
239
+ a.name = "COHEN, WEISS & SIMON LLP"
240
+ assert a.save!
241
+ assert a.delete
242
+ end
152
243
  end
153
244
 
154
245
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sugarcrm
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 41
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
8
  - 9
8
- - 8
9
- version: 0.9.8
9
+ - 9
10
+ version: 0.9.9
10
11
  platform: ruby
11
12
  authors:
12
13
  - Carl Hicks
@@ -14,93 +15,99 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2011-01-18 00:00:00 -08:00
18
+ date: 2011-01-30 00:00:00 -08:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
22
+ type: :runtime
23
+ prerelease: false
21
24
  name: activesupport
22
- requirement: &id001 !ruby/object:Gem::Requirement
25
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
26
  none: false
24
27
  requirements:
25
28
  - - ">="
26
29
  - !ruby/object:Gem::Version
30
+ hash: 7
27
31
  segments:
28
32
  - 3
29
33
  - 0
30
34
  - 0
31
35
  version: 3.0.0
36
+ requirement: *id001
37
+ - !ruby/object:Gem::Dependency
32
38
  type: :runtime
33
39
  prerelease: false
34
- version_requirements: *id001
35
- - !ruby/object:Gem::Dependency
36
40
  name: i18n
37
- requirement: &id002 !ruby/object:Gem::Requirement
41
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
42
  none: false
39
43
  requirements:
40
44
  - - ">="
41
45
  - !ruby/object:Gem::Version
46
+ hash: 3
42
47
  segments:
43
48
  - 0
44
49
  version: "0"
50
+ requirement: *id002
51
+ - !ruby/object:Gem::Dependency
45
52
  type: :runtime
46
53
  prerelease: false
47
- version_requirements: *id002
54
+ name: json
55
+ version_requirements: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirement: *id003
48
65
  - !ruby/object:Gem::Dependency
66
+ type: :development
67
+ prerelease: false
49
68
  name: shoulda
50
- requirement: &id003 !ruby/object:Gem::Requirement
69
+ version_requirements: &id004 !ruby/object:Gem::Requirement
51
70
  none: false
52
71
  requirements:
53
72
  - - ">="
54
73
  - !ruby/object:Gem::Version
74
+ hash: 3
55
75
  segments:
56
76
  - 0
57
77
  version: "0"
78
+ requirement: *id004
79
+ - !ruby/object:Gem::Dependency
58
80
  type: :development
59
81
  prerelease: false
60
- version_requirements: *id003
61
- - !ruby/object:Gem::Dependency
62
82
  name: bundler
63
- requirement: &id004 !ruby/object:Gem::Requirement
83
+ version_requirements: &id005 !ruby/object:Gem::Requirement
64
84
  none: false
65
85
  requirements:
66
86
  - - ~>
67
87
  - !ruby/object:Gem::Version
88
+ hash: 23
68
89
  segments:
69
90
  - 1
70
91
  - 0
71
92
  - 0
72
93
  version: 1.0.0
94
+ requirement: *id005
95
+ - !ruby/object:Gem::Dependency
73
96
  type: :development
74
97
  prerelease: false
75
- version_requirements: *id004
76
- - !ruby/object:Gem::Dependency
77
98
  name: jeweler
78
- requirement: &id005 !ruby/object:Gem::Requirement
99
+ version_requirements: &id006 !ruby/object:Gem::Requirement
79
100
  none: false
80
101
  requirements:
81
102
  - - ~>
82
103
  - !ruby/object:Gem::Version
104
+ hash: 7
83
105
  segments:
84
106
  - 1
85
107
  - 5
86
108
  - 2
87
109
  version: 1.5.2
88
- type: :development
89
- prerelease: false
90
- version_requirements: *id005
91
- - !ruby/object:Gem::Dependency
92
- name: rcov
93
- requirement: &id006 !ruby/object:Gem::Requirement
94
- none: false
95
- requirements:
96
- - - ">="
97
- - !ruby/object:Gem::Version
98
- segments:
99
- - 0
100
- version: "0"
101
- type: :development
102
- prerelease: false
103
- version_requirements: *id006
110
+ requirement: *id006
104
111
  description: A less clunky way to interact with SugarCRM via REST. Instead of SugarCRM.connection.get_entry("Users", "1") you could use SugarCRM::User.find(1). There is support for collections a la SugarCRM::User.find(1).email_addresses, or SugarCRM::Contact.first.meetings << new_meeting. ActiveRecord style finders are in place, with limited support for conditions and joins.
105
112
  email: carl.hicks@gmail.com
106
113
  executables: []
@@ -130,6 +137,7 @@ files:
130
137
  - lib/sugarcrm/attributes/attribute_typecast.rb
131
138
  - lib/sugarcrm/attributes/attribute_validations.rb
132
139
  - lib/sugarcrm/base.rb
140
+ - lib/sugarcrm/config/sugarcrm.yaml
133
141
  - lib/sugarcrm/connection.rb
134
142
  - lib/sugarcrm/connection/api/get_available_modules.rb
135
143
  - lib/sugarcrm/connection/api/get_document_revision.rb
@@ -160,9 +168,12 @@ files:
160
168
  - lib/sugarcrm/connection/request.rb
161
169
  - lib/sugarcrm/connection/response.rb
162
170
  - lib/sugarcrm/dynamic_finder_match.rb
171
+ - lib/sugarcrm/environment.rb
163
172
  - lib/sugarcrm/exceptions.rb
173
+ - lib/sugarcrm/extensions/README.txt
164
174
  - lib/sugarcrm/module.rb
165
175
  - lib/sugarcrm/module_methods.rb
176
+ - test/config_test.yaml
166
177
  - test/connection/test_get_available_modules.rb
167
178
  - test/connection/test_get_entries.rb
168
179
  - test/connection/test_get_entry.rb
@@ -175,11 +186,13 @@ files:
175
186
  - test/connection/test_login.rb
176
187
  - test/connection/test_logout.rb
177
188
  - test/connection/test_set_relationship.rb
189
+ - test/extensions_test/patch.rb
178
190
  - test/helper.rb
179
191
  - test/test_association.rb
180
192
  - test/test_association_collection.rb
181
193
  - test/test_associations.rb
182
194
  - test/test_connection.rb
195
+ - test/test_environment.rb
183
196
  - test/test_module.rb
184
197
  - test/test_response.rb
185
198
  - test/test_sugarcrm.rb
@@ -197,7 +210,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
197
210
  requirements:
198
211
  - - ">="
199
212
  - !ruby/object:Gem::Version
200
- hash: -1258950620288984122
213
+ hash: 3
201
214
  segments:
202
215
  - 0
203
216
  version: "0"
@@ -206,6 +219,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
219
  requirements:
207
220
  - - ">="
208
221
  - !ruby/object:Gem::Version
222
+ hash: 3
209
223
  segments:
210
224
  - 0
211
225
  version: "0"
@@ -229,11 +243,13 @@ test_files:
229
243
  - test/connection/test_login.rb
230
244
  - test/connection/test_logout.rb
231
245
  - test/connection/test_set_relationship.rb
246
+ - test/extensions_test/patch.rb
232
247
  - test/helper.rb
233
248
  - test/test_association.rb
234
249
  - test/test_association_collection.rb
235
250
  - test/test_associations.rb
236
251
  - test/test_connection.rb
252
+ - test/test_environment.rb
237
253
  - test/test_module.rb
238
254
  - test/test_response.rb
239
255
  - test/test_sugarcrm.rb