sugarcrm 0.9.10 → 0.9.11

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 (69) hide show
  1. data/LICENSE +1 -1
  2. data/README.rdoc +38 -6
  3. data/Rakefile +1 -0
  4. data/VERSION +1 -1
  5. data/bin/sugarcrm +26 -0
  6. data/lib/sugarcrm.rb +2 -2
  7. data/lib/sugarcrm/associations/association.rb +11 -8
  8. data/lib/sugarcrm/associations/association_collection.rb +1 -1
  9. data/lib/sugarcrm/associations/association_methods.rb +1 -1
  10. data/lib/sugarcrm/attributes/attribute_methods.rb +7 -2
  11. data/lib/sugarcrm/attributes/attribute_typecast.rb +2 -2
  12. data/lib/sugarcrm/base.rb +37 -236
  13. data/lib/sugarcrm/connection/api/get_available_modules.rb +2 -2
  14. data/lib/sugarcrm/connection/api/get_document_revision.rb +2 -2
  15. data/lib/sugarcrm/connection/api/get_entries.rb +2 -2
  16. data/lib/sugarcrm/connection/api/get_entries_count.rb +1 -1
  17. data/lib/sugarcrm/connection/api/get_entry.rb +2 -2
  18. data/lib/sugarcrm/connection/api/get_entry_list.rb +2 -2
  19. data/lib/sugarcrm/connection/api/get_module_fields.rb +2 -2
  20. data/lib/sugarcrm/connection/api/get_note_attachment.rb +1 -1
  21. data/lib/sugarcrm/connection/api/get_relationships.rb +2 -2
  22. data/lib/sugarcrm/connection/api/get_report_entries.rb +1 -1
  23. data/lib/sugarcrm/connection/api/get_server_info.rb +1 -1
  24. data/lib/sugarcrm/connection/api/get_user_id.rb +1 -1
  25. data/lib/sugarcrm/connection/api/get_user_team_id.rb +1 -1
  26. data/lib/sugarcrm/connection/api/logout.rb +1 -1
  27. data/lib/sugarcrm/connection/api/seamless_login.rb +1 -1
  28. data/lib/sugarcrm/connection/api/search_by_module.rb +1 -1
  29. data/lib/sugarcrm/connection/api/set_campaign_merge.rb +1 -1
  30. data/lib/sugarcrm/connection/api/set_document_revision.rb +1 -1
  31. data/lib/sugarcrm/connection/api/set_entries.rb +1 -1
  32. data/lib/sugarcrm/connection/api/set_entry.rb +1 -1
  33. data/lib/sugarcrm/connection/api/set_note_attachment.rb +1 -1
  34. data/lib/sugarcrm/connection/api/set_relationship.rb +1 -1
  35. data/lib/sugarcrm/connection/api/set_relationships.rb +1 -1
  36. data/lib/sugarcrm/connection/connection.rb +5 -10
  37. data/lib/sugarcrm/connection/helper.rb +3 -2
  38. data/lib/sugarcrm/connection/response.rb +8 -6
  39. data/lib/sugarcrm/exceptions.rb +3 -0
  40. data/lib/sugarcrm/finders.rb +2 -0
  41. data/lib/sugarcrm/{dynamic_finder_match.rb → finders/dynamic_finder_match.rb} +0 -0
  42. data/lib/sugarcrm/finders/finder_methods.rb +236 -0
  43. data/lib/sugarcrm/module.rb +35 -11
  44. data/lib/sugarcrm/module_methods.rb +68 -23
  45. data/lib/sugarcrm/session.rb +179 -0
  46. data/test/connection/test_get_available_modules.rb +1 -4
  47. data/test/connection/test_get_entries.rb +2 -8
  48. data/test/connection/test_get_entry.rb +1 -2
  49. data/test/connection/test_get_entry_list.rb +6 -12
  50. data/test/connection/test_get_module_fields.rb +1 -4
  51. data/test/connection/test_get_relationships.rb +1 -4
  52. data/test/connection/test_get_server_info.rb +1 -4
  53. data/test/connection/test_get_user_id.rb +1 -4
  54. data/test/connection/test_get_user_team_id.rb +1 -4
  55. data/test/connection/test_login.rb +3 -5
  56. data/test/connection/test_logout.rb +1 -4
  57. data/test/connection/test_set_note_attachment.rb +1 -2
  58. data/test/connection/test_set_relationship.rb +1 -2
  59. data/test/helper.rb +6 -8
  60. data/test/test_association_collection.rb +1 -2
  61. data/test/test_associations.rb +18 -1
  62. data/test/test_connection.rb +2 -6
  63. data/test/test_module.rb +34 -6
  64. data/test/test_response.rb +2 -3
  65. data/test/test_session.rb +109 -0
  66. data/test/test_sugarcrm.rb +58 -7
  67. metadata +14 -11
  68. data/lib/sugarcrm/environment.rb +0 -63
  69. data/test/test_environment.rb +0 -45
@@ -12,7 +12,8 @@ module SugarCRM
12
12
  # Dynamically register objects based on Module name
13
13
  # I.e. a SugarCRM Module named Users will generate
14
14
  # a SugarCRM::User class.
15
- def initialize(name)
15
+ def initialize(session, name)
16
+ @session = session # the session from which this module was retrieved
16
17
  @name = name
17
18
  @klass = name.classify
18
19
  unless custom_module?
@@ -50,7 +51,7 @@ module SugarCRM
50
51
  # Returns the fields associated with the module
51
52
  def fields
52
53
  return @fields if fields_registered?
53
- all_fields = SugarCRM.connection.get_fields(@name)
54
+ all_fields = @session.connection.get_fields(@name)
54
55
  @fields = all_fields["module_fields"].with_indifferent_access
55
56
  @link_fields= all_fields["link_fields"]
56
57
  handle_empty_arrays
@@ -94,20 +95,30 @@ module SugarCRM
94
95
  def register
95
96
  return self if registered?
96
97
  mod_instance = self
98
+ sess = @session
97
99
  # class Class < SugarCRM::Base
98
100
  # module_name = "Accounts"
99
101
  # end
100
102
  klass = Class.new(SugarCRM::Base) do
101
103
  self._module = mod_instance
102
- end
104
+ self.session = sess
105
+ end
103
106
 
104
107
  # class Account < SugarCRM::Base
105
- SugarCRM.const_set self.klass, klass
108
+ @session.namespace_const.const_set self.klass, klass
106
109
  self
107
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
108
119
 
109
120
  def registered?
110
- SugarCRM.const_defined? @klass
121
+ @session.namespace_const.const_defined? @klass
111
122
  end
112
123
 
113
124
  def to_s
@@ -122,18 +133,31 @@ module SugarCRM
122
133
  @initialized = false
123
134
 
124
135
  # Registers all of the SugarCRM Modules
125
- def register_all
126
- SugarCRM.connection.get_modules.each do |m|
127
- SugarCRM.modules << m.register
136
+ def register_all(session)
137
+ namespace = session.namespace_const
138
+ session.connection.get_modules.each do |m|
139
+ session.modules << m.register
128
140
  end
129
141
  @initialized = true
130
142
  true
131
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
132
155
 
133
156
  # Finds a module by name, or klass name
134
- def find(name)
135
- register_all unless initialized?
136
- SugarCRM.modules.each do |m|
157
+ def find(name, session=nil)
158
+ session ||= SugarCRM.session
159
+ register_all(session) unless initialized?
160
+ session.modules.each do |m|
137
161
  return m if m.name == name
138
162
  return m if m.klass == name
139
163
  end
@@ -1,29 +1,69 @@
1
1
  module SugarCRM
2
+ # store the namespaces that have been used to prevent namespace collision
3
+ @@used_namespaces = []
4
+ def self.used_namespaces
5
+ @@used_namespaces
6
+ end
2
7
 
3
- @@connection = nil
4
- def self.connection
5
- @@connection
8
+ # return the namespaces linked to active sessions
9
+ def self.namespaces
10
+ result = []
11
+ @@used_namespaces.each{|n|
12
+ result << n if SugarCRM.const_defined? n
13
+ }
14
+ result
15
+ end
16
+
17
+ # store the various connected sessions
18
+ # key = session.id, value = session instance
19
+ @@sessions = {}
20
+ def self.sessions
21
+ @@sessions
22
+ end
23
+
24
+ def self.add_session(session)
25
+ @@used_namespaces << session.namespace unless @@used_namespaces.include? session.namespace
26
+ @@sessions[session.id] = session
27
+ end
28
+
29
+ def self.remove_session(session)
30
+ @@sessions.delete(session.id)
31
+ end
32
+
33
+ def self.session
34
+ return nil if @@sessions.size < 1
35
+ (raise SugarCRM::MultipleSessions, "There are multiple active sessions: use the session namespace instead of SugarCRM") if @@sessions.size > 1
36
+ @@sessions.values.first
6
37
  end
7
- def self.connection=(connection)
8
- @@connection = connection
38
+
39
+ def self.connection
40
+ return nil unless self.session
41
+ self.session.connection
9
42
  end
10
- def self.connect(url=SugarCRM::Environment.config[:base_url], user=SugarCRM::Environment.config[:username], pass=SugarCRM::Environment.config[:password], options={})
11
- SugarCRM::Base.establish_connection(url, user, pass, options)
43
+
44
+ def self.connect(url, user, pass, options={})
45
+ session = SugarCRM::Session.new(url, user, pass, options)
46
+ # return the namespace module
47
+ session.namespace_const
12
48
  end
49
+
13
50
  class << self
14
51
  alias :connect! :connect
15
52
  end
16
53
 
17
- @@modules = []
18
- def self.modules
19
- @@modules
20
- end
21
- def self.modules=(modules)
22
- @@modules = modules
54
+ def respond_to?(sym)
55
+ return true if @@sessions.size == 1 && SugarCRM.session.namespace_const.respond_to?(sym)
56
+ super
23
57
  end
24
58
 
25
- def self.current_user
26
- SugarCRM::User.find_by_user_name(connection.user)
59
+ def self.method_missing(sym, *args, &block)
60
+ (raise SugarCRM::NoActiveSession, "No session is active. Create a new session with 'SugarCRM.connect(...)'") if @@sessions.size < 1
61
+ (raise SugarCRM::MultipleSessions, "There are multiple active sessions: call methods on the session namespace instead of SugarCRM") if @@sessions.size > 1
62
+ if SugarCRM.session.namespace_const.respond_to? sym
63
+ SugarCRM.session.namespace_const.send(sym, *args, &block)
64
+ else
65
+ super
66
+ end
27
67
  end
28
68
 
29
69
  # If a user tries to access a SugarCRM class before they're logged in,
@@ -31,16 +71,21 @@ module SugarCRM
31
71
  # This will trigger module loading,
32
72
  # and we can then attempt to return the requested class automagically
33
73
  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?
74
+ (raise SugarCRM::MultipleSessions, "There are multiple active sessions: use the session namespace instead of SugarCRM") if @@sessions.size > 1
75
+ # make sure we have an active session
76
+ begin
77
+ Session.new unless SugarCRM.connection && SugarCRM.connection.logged_in?
78
+ rescue SugarCRM::MissingCredentials => e
79
+ # unable to load necessary login credentials from config file => pass exception on
36
80
  super
81
+ end
82
+
83
+ # if user calls (e.g.) SugarCRM::Account, delegate to SugarCRM::Namespace0::Account
84
+ namespace_const = SugarCRM.session.namespace_const
85
+ if namespace_const.const_defined? sym
86
+ namespace_const.const_get(sym)
37
87
  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)
88
+ super
44
89
  end
45
90
  end
46
91
  end
@@ -0,0 +1,179 @@
1
+ # This class hold an individual connection to a SugarCRM server.
2
+ # There can be several such simultaneous connections
3
+ module SugarCRM; class Session
4
+ attr_reader :config, :connection, :extensions_path, :id, :namespace, :namespace_const
5
+ attr_accessor :modules
6
+ def initialize(url, user, pass, opts={})
7
+ options = {
8
+ :debug => false,
9
+ :register_modules => true
10
+ }.merge(opts)
11
+
12
+ @modules = []
13
+ @namespace = "Namespace#{SugarCRM.used_namespaces.size}"
14
+ @config = {:base_url => url,:username => user,:password => pass,:options => options}
15
+ @extensions_path = File.join(File.dirname(__FILE__), 'extensions')
16
+
17
+ setup_connection
18
+ register_namespace
19
+ connect_and_add_session
20
+ end
21
+
22
+ # create a new session from the credentials present in a file
23
+ def self.new_from_file(path, opts={})
24
+ config = load_and_parse_config(path)
25
+ begin
26
+ session = self.new(config[:base_url], config[:username], config[:password], opts)
27
+ rescue MissingCredentials => e
28
+ return false
29
+ end
30
+ session.namespace_const
31
+ end
32
+
33
+ def setup_connection
34
+ load_config_files unless connection_info_loaded?
35
+ raise MissingCredentials, "Missing login credentials. Make sure you provide the SugarCRM URL, username, and password" unless connection_info_loaded?
36
+ end
37
+
38
+ # re-use this session and namespace if the user wants to connect with different credentials
39
+ def connect(url=nil, user=nil, pass=nil, opts={})
40
+ options = {
41
+ :debug => (@connection && @connection.debug?),
42
+ :register_modules => true
43
+ }.merge(opts)
44
+
45
+ {:base_url => url, :username => user, :password => pass}.each{|k,v|
46
+ @config[k] = v unless v.nil?
47
+ }
48
+
49
+ SugarCRM::Module.deregister_all(self)
50
+ SugarCRM.remove_session(self)
51
+ @connection = SugarCRM::Connection.new(@config[:base_url], @config[:username], @config[:password], options) if connection_info_loaded?
52
+ @connection.session = self
53
+ @id = @connection.session_id
54
+ SugarCRM.add_session(self) # must be removed and added back, as the session id (used as the hash key) changes
55
+ SugarCRM::Module.register_all(self)
56
+ load_extensions
57
+ true
58
+ end
59
+ alias :connect! :connect
60
+ alias :reconnect :connect
61
+ alias :reconnect! :connect
62
+ alias :reload! :connect
63
+
64
+ # log out from SugarCRM and cleanup (deregister modules, remove session, etc.)
65
+ def disconnect
66
+ @connection.logout
67
+ SugarCRM::Module.deregister_all(self)
68
+ namespace = @namespace
69
+ SugarCRM.instance_eval{ remove_const namespace } # remove NamespaceX from SugarCRM
70
+ SugarCRM.remove_session(self)
71
+ end
72
+ alias :disconnect! :disconnect
73
+
74
+ def extensions_folder=(folder, dirstring=nil)
75
+ path = File.expand_path(folder, dirstring)
76
+ @extensions_path = path
77
+ load_extensions
78
+ end
79
+
80
+ # load credentials from file, and (re)connect to SugarCRM
81
+ def load_config(path)
82
+ @config = self.class.load_and_parse_config(path)
83
+ reconnect(@config[:base_url], @config[:username], @config[:password]) if connection_info_loaded?
84
+ @config
85
+ end
86
+
87
+ def update_config(params)
88
+ params.each{|k,v|
89
+ @config[k.to_sym] = v
90
+ }
91
+ @config
92
+ end
93
+
94
+ # lazy load the SugarCRM version we're connecting to
95
+ def sugar_version
96
+ @version ||= @connection.get_server_info["version"]
97
+ end
98
+
99
+ private
100
+ def self.load_and_parse_config(path)
101
+ validate_path path
102
+ hash = {}
103
+ config = YAML.load_file(path)
104
+ if config && config["config"]
105
+ config["config"].each{|k,v|
106
+ hash[k.to_sym] = v
107
+ }
108
+ end
109
+ hash
110
+ end
111
+
112
+ def self.validate_path(path)
113
+ raise "Invalid path: #{path}" unless File.exists? path
114
+ end
115
+
116
+ def config_file_paths
117
+ # see README for reasoning behind the priorization
118
+ paths = ['/etc/sugarcrm.yaml', File.expand_path('~/.sugarcrm.yaml'), File.join(File.dirname(__FILE__), 'config', 'sugarcrm.yaml')]
119
+ paths.insert(1, File.join(ENV['USERPROFILE'], 'sugarcrm.yaml')) if ENV['USERPROFILE']
120
+ paths
121
+ end
122
+
123
+ def connection_info_loaded?
124
+ @config[:base_url] && @config[:username] && @config[:password]
125
+ end
126
+
127
+ def generate_namespace_module
128
+ # Create a new module to have a separate namespace in which to register the SugarCRM modules.
129
+ # This will prevent issues with modules from separate SugarCRM instances colliding within the same namespace
130
+ # (e.g. 2 SugarCRM instances where only one has custom fields on the Account module)
131
+ namespace_module = Object::Module.new do
132
+ @session = nil
133
+ def self.session
134
+ @session
135
+ end
136
+ def self.session=(sess)
137
+ @session = sess
138
+ end
139
+ def self.current_user
140
+ (@session.namespace_const)::User.find_by_user_name(@session.config[:username])
141
+ end
142
+ def self.respond_to?(sym)
143
+ return true if @session.respond_to? sym
144
+ super
145
+ end
146
+ def self.method_missing(sym, *args, &block)
147
+ raise unless @session.respond_to? sym
148
+ @session.send(sym, *args, &block)
149
+ end
150
+ end
151
+ # set the session: will be needed in SugarCRM::Base to call the API methods on the correct session
152
+ namespace_module.session = self
153
+ namespace_module
154
+ end
155
+
156
+ def load_config_files
157
+ # see README for reasoning behind the priorization
158
+ config_file_paths.each{|path|
159
+ load_config path if File.exists? path
160
+ }
161
+ end
162
+
163
+ def register_namespace
164
+ SugarCRM.const_set(@namespace, generate_namespace_module)
165
+ @namespace_const = SugarCRM.const_get(@namespace)
166
+ end
167
+
168
+ # load all the monkey patch extension files in the provided folder
169
+ def load_extensions
170
+ self.class.validate_path @extensions_path
171
+ Dir[File.join(@extensions_path, '**', '*.rb').to_s].each { |f| load(f) }
172
+ end
173
+
174
+ def connect_and_add_session
175
+ connect(@config[:base_url], @config[:username], @config[:password], @config[:options])
176
+ SugarCRM.add_session(self)
177
+ end
178
+
179
+ end; end
@@ -1,10 +1,7 @@
1
1
  require 'helper'
2
2
 
3
- class TestGetAvailableModules < Test::Unit::TestCase
3
+ class TestGetAvailableModules < ActiveSupport::TestCase
4
4
  context "A SugarCRM.connection" do
5
- setup do
6
- SugarCRM::Connection.new(URL, USER, PASS, {:debug => false})
7
- end
8
5
  should "return an array of modules when #get_modules" do
9
6
  assert_instance_of SugarCRM::Module, SugarCRM.connection.get_modules[0]
10
7
  end
@@ -1,19 +1,13 @@
1
1
  require 'helper'
2
2
 
3
- class TestGetEntries < Test::Unit::TestCase
3
+ class TestGetEntries < ActiveSupport::TestCase
4
4
  context "A SugarCRM.connection" do
5
- setup do
6
- SugarCRM::Connection.new(URL, USER, PASS, {:register_modules => true, :debug => false})
5
+ should "return an object when #get_entries" do
7
6
  @response = SugarCRM.connection.get_entries(
8
7
  "Users",
9
8
  [1,"seed_sally_id"],
10
9
  {:fields => ["first_name", "last_name"]}
11
10
  )
12
- end
13
- # should "return a list of entries when sent #get_entries." do
14
- # assert @response.response.key? "entry_list"
15
- # end
16
- should "return an object when #get_entries" do
17
11
  assert_instance_of Array, @response
18
12
  assert_instance_of SugarCRM::User, @response[0]
19
13
  end
@@ -1,9 +1,8 @@
1
1
  require 'helper'
2
2
 
3
- class TestGetEntry < Test::Unit::TestCase
3
+ class TestGetEntry < ActiveSupport::TestCase
4
4
  context "A SugarCRM.connection" do
5
5
  setup do
6
- SugarCRM::Connection.new(URL, USER, PASS, {:debug => false })
7
6
  @response = SugarCRM.connection.get_entry(
8
7
  "Users",
9
8
  1,
@@ -1,18 +1,7 @@
1
1
  require 'helper'
2
2
 
3
- class TestGetEntryList < Test::Unit::TestCase
3
+ class TestGetEntryList < ActiveSupport::TestCase
4
4
  context "A SugarCRM.connection" do
5
- setup do
6
- SugarCRM::Connection.new(URL, USER, PASS, {:register_modules => true, :debug => false})
7
- @response = SugarCRM.connection.get_entry_list(
8
- "Users",
9
- "users.deleted = 0",
10
- {:fields => ["first_name", "last_name"]}
11
- )
12
- end
13
- # should "return a list of entries when sent #get_entry_list." do
14
- # assert @response.response.key? "entry_list"
15
- # end
16
5
  should "return a list of entries when sent #get_entry_list and no fields." do
17
6
  users = SugarCRM.connection.get_entry_list(
18
7
  "Users",
@@ -21,6 +10,11 @@ class TestGetEntryList < Test::Unit::TestCase
21
10
  assert_equal "admin", users.first.user_name
22
11
  end
23
12
  should "return an object when #get_entry_list" do
13
+ @response = SugarCRM.connection.get_entry_list(
14
+ "Users",
15
+ "users.deleted = 0",
16
+ {:fields => ["first_name", "last_name"]}
17
+ )
24
18
  assert_instance_of Array, @response
25
19
  assert_instance_of SugarCRM::User, @response[0]
26
20
  end