sugarcrm 0.9.8 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
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