sugarcrm 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.7
1
+ 0.9.8
@@ -1,2 +1,5 @@
1
+ require 'sugarcrm/associations/association'
2
+ require 'sugarcrm/associations/associations'
3
+ require 'sugarcrm/associations/association_cache'
1
4
  require 'sugarcrm/associations/association_methods'
2
5
  require 'sugarcrm/associations/association_collection'
@@ -0,0 +1,104 @@
1
+ module SugarCRM
2
+ # Represents an association and it's metadata
3
+ class Association
4
+ attr :owner
5
+ attr :target
6
+ attr :link_field
7
+ attr :attributes
8
+ attr :methods
9
+
10
+ def initialize(owner,link_field,opts={})
11
+ @options = { :define_methods? => true }.merge! opts
12
+ @owner = owner
13
+ check_valid_owner
14
+ @link_field = link_field
15
+ @attributes = owner.link_fields[link_field]
16
+ @target = resolve_target
17
+ @methods = define_methods if @options[:define_methods?]
18
+ self
19
+ end
20
+
21
+ # Returns true if the association includes an attribute that matches
22
+ # the provided string
23
+ def include?(attribute)
24
+ return true if attribute.class == @target
25
+ return true if attribute == link_field
26
+ return true if methods.include? attribute
27
+ false
28
+ end
29
+
30
+ protected
31
+
32
+ def check_valid_owner
33
+ valid = @owner.class.ancestors.include? SugarCRM::Base
34
+ raise InvalidModule, "#{@owner} is not registered, or is not a descendant of SugarCRM::Base" unless valid
35
+ end
36
+
37
+ # Attempts to determine the class of the target in the association
38
+ def resolve_target
39
+ # Use the link_field name first
40
+ klass = @link_field.singularize.camelize
41
+ return "SugarCRM::#{klass}".constantize if SugarCRM.const_defined? klass
42
+
43
+ # Use the link_field attribute "module"
44
+ if @attributes["module"].length > 0
45
+ module_name = SugarCRM::Module.find(@attributes["module"])
46
+ return "SugarCRM::#{module_name.klass}".constantize if SugarCRM.const_defined? module_name.klass
47
+ end
48
+ # Use the link_field attribute "relationship"
49
+ if @attributes["relationship"].length > 0
50
+ klass = humanized_link_name(@attributes["relationship"]).singularize.camelize
51
+ return "SugarCRM::#{klass}".constantize if SugarCRM.const_defined? klass
52
+ end
53
+ false
54
+ end
55
+
56
+ # Generates the association proxy method for related module
57
+ def define_method(link_field, pretty_name=nil)
58
+ pretty_name ||= link_field
59
+ @owner.class.module_eval %Q?
60
+ def #{pretty_name}
61
+ query_association :#{link_field}
62
+ end
63
+ ?
64
+ pretty_name
65
+ end
66
+
67
+ # Defines methods for accessing the association target on the owner class.
68
+ # If the link_field name includes the owner class name, it is stripped before
69
+ # creating the method. If this occurs, we also create an alias to the stripped
70
+ # method using the full link_field name.
71
+ def define_methods
72
+ methods = []
73
+ pretty_name = humanized_link_name(@link_field)
74
+ methods << define_method(pretty_name)
75
+ if pretty_name != @link_field
76
+ @owner.class.module_eval %Q?
77
+ alias :#{@link_field} #{pretty_name}
78
+ ?
79
+ methods << @link_field
80
+ end
81
+ methods
82
+ end
83
+
84
+ # Return the name of the relationship excluding the owner part of the name.
85
+ # e.g. if a custom relationship is defined in Studio between Tasks and Documents,
86
+ # the link_field will be `tasks_documents` but a human would call the relationship `documents`
87
+ 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('_')
100
+ end
101
+
102
+ end
103
+ end
104
+
@@ -0,0 +1,30 @@
1
+ module SugarCRM; module AssociationCache
2
+
3
+ attr :association_cache, false
4
+
5
+ # Returns true if an association is cached
6
+ def association_cached?(association)
7
+ @association_cache.symbolize_keys.include? association.to_sym
8
+ end
9
+
10
+ protected
11
+
12
+ # Returns true if an association collection has changed
13
+ def associations_changed?
14
+ @association_cache.values.each do |collection|
15
+ return true if collection.changed?
16
+ end
17
+ false
18
+ end
19
+
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
25
+
26
+ # Resets the association cache
27
+ def clear_association_cache
28
+ @association_cache = {}.with_indifferent_access
29
+ end
30
+ end; end
@@ -12,7 +12,7 @@ module SugarCRM
12
12
  load if preload
13
13
  self
14
14
  end
15
-
15
+
16
16
  def changed?
17
17
  return false unless loaded?
18
18
  return true if added.length > 0
@@ -95,8 +95,7 @@ module SugarCRM
95
95
  removed.each do |record|
96
96
  disassociate!(record)
97
97
  end
98
- @original = @collection.dup
99
- @original.freeze
98
+ reload
100
99
  true
101
100
  end
102
101
 
@@ -118,12 +117,12 @@ module SugarCRM
118
117
  # user would be the owner, and EmailAddress.new() is the target
119
118
  def associate!(target, opts={})
120
119
  #target.save! if target.new?
121
- @owner.associate!(target, [target.id], opts)
120
+ @owner.associate!(target, opts)
122
121
  end
123
122
 
124
123
  # Removes a relationship between the current object and the target
125
124
  def disassociate!(target)
126
- associate!(target,{:delete => 1})
125
+ @owner.associate!(target,{:delete => 1})
127
126
  end
128
127
 
129
128
  alias :relate! :associate!
@@ -1,74 +1,53 @@
1
1
  module SugarCRM; module AssociationMethods
2
-
2
+
3
3
  module ClassMethods
4
- # Returns an array of the module link fields
5
- def associations_from_module_link_fields
6
- self._module.link_fields.keys
7
- end
8
- end
9
-
10
- attr :association_cache, false
11
-
12
- def association_cached?(association)
13
- @association_cache.keys.include? association.to_sym
14
4
  end
15
-
16
- def associations_changed?
5
+
6
+ # Saves all modified associations.
7
+ def save_modified_associations!
17
8
  @association_cache.values.each do |collection|
18
- return true if collection.changed?
9
+ if collection.changed?
10
+ collection.save!
11
+ end
19
12
  end
20
- false
13
+ true
21
14
  end
22
15
 
23
- # Creates a relationship between the current object and the target
24
- # The current instance and target records will have a relationship set
25
- # i.e. account.associate!(contact) wyould link account and contact
16
+ # Returns the module link fields hash
17
+ def link_fields
18
+ self.class._module.link_fields
19
+ end
20
+
21
+ # Creates a relationship between the current object and the target object
22
+ # The current and target records will have a relationship set
23
+ # i.e. account.associate!(contact) would link account and contact
26
24
  # In contrast to using account.contacts << contact, this method doesn't load the relationships
27
25
  # before setting the new relationship.
28
26
  # This method is useful when certain modules have many links to other modules: not loading the
29
27
  # relationships allows one ot avoid a Timeout::Error
30
- def associate!(target, target_ids=[], opts={})
31
- if self.class._module.custom_module? || target.class._module.custom_module?
32
- link_field = get_link_field(target)
33
- else
34
- link_field = target.class._module.table_name
35
- end
36
- target_ids = [target.id] if target_ids.size < 1
37
- response = SugarCRM.connection.set_relationship(
38
- self.class._module.name, self.id,
39
- link_field, target_ids,
40
- opts
41
- )
42
- raise AssociationFailed,
43
- "Couldn't associate #{self.class._module.name}: #{self.id} -> #{target.class._module.table_name}:#{target.id}!" if response["failed"] > 0
44
- true
45
- end
46
-
47
- protected
48
-
49
- def save_modified_associations
50
- @association_cache.values.each do |collection|
51
- if collection.changed?
52
- return false unless collection.save
28
+ def associate!(target,opts={})
29
+ targets = Array.wrap(target)
30
+ targets.each do |t|
31
+ association = @associations.find!(t)
32
+ response = SugarCRM.connection.set_relationship(
33
+ self.class._module.name, self.id,
34
+ association.link_field, [t.id], opts
35
+ )
36
+ if response["failed"] > 0
37
+ raise AssociationFailed,
38
+ "Couldn't associate #{self.class._module.name}: #{self.id} -> #{t}: #{t.id}!"
53
39
  end
40
+ update_association_cache_for(association.link_field, t)
54
41
  end
55
42
  true
56
43
  end
57
-
58
- def clear_association_cache
59
- @association_cache = {}
60
- end
61
-
44
+
45
+ protected
46
+
62
47
  # Generates the association proxy methods for related modules
63
48
  def define_association_methods
49
+ @associations = Associations.register(self)
64
50
  return if association_methods_generated?
65
- @associations.each do |k|
66
- self.class.module_eval %Q?
67
- def #{k}
68
- query_association :#{k}
69
- end
70
- ?
71
- end
72
51
  self.class.association_methods_generated = true
73
52
  end
74
53
 
@@ -94,15 +73,5 @@ module SugarCRM; module AssociationMethods
94
73
  @association_cache[association] = collection
95
74
  collection
96
75
  end
97
-
98
- # return the link field involving a relationship with a custom module
99
- def get_link_field(other)
100
- this_table_name = self.class._module.custom_module? ? self.class._module.name : self.class._module.table_name
101
- that_table_name = other.class._module.custom_module? ? other.class._module.name : other.class._module.table_name
102
- # the link field will contain the name of both modules
103
- link_field = self.associations.detect{|a| a == [this_table_name, that_table_name].join('_') || a == [that_table_name, this_table_name].join('_')}
104
- raise "Unable to determine link field between #{self.class._module.name}: #{self.id} and #{other.class._module.table_name}:#{other.id}" unless link_field
105
- link_field
106
- end
107
76
 
108
77
  end; end
@@ -0,0 +1,62 @@
1
+ module SugarCRM
2
+ # Holds all the associations for a given class
3
+ class Associations
4
+ # Returns an array of Association objects
5
+ class << self
6
+ def register(owner)
7
+ associations = Associations.new
8
+ owner.link_fields.each_key do |link_field|
9
+ associations << Association.new(owner,link_field)
10
+ end
11
+ associations
12
+ end
13
+ end
14
+
15
+ attr :associations
16
+
17
+ def initialize
18
+ @associations = Set.new
19
+ self
20
+ end
21
+
22
+ # Looks up an association by object, link_field, or method.
23
+ # Raises an exception if not found
24
+ def find!(target)
25
+ @associations.each do |a|
26
+ return a if a.include? target
27
+ end
28
+ raise InvalidAssociation, "Could not lookup association for: #{target}"
29
+ end
30
+
31
+ # Looks up an association by object, link_field, or method.
32
+ # Returns false if not found
33
+ def find(association)
34
+ begin
35
+ find!
36
+ rescue InvalidAssociation
37
+ false
38
+ end
39
+ end
40
+
41
+ def inspect
42
+ self
43
+ end
44
+
45
+ def to_s
46
+ methods = []
47
+ @associations.each do |a|
48
+ a.methods.each do |m|
49
+ methods << m
50
+ end
51
+ end
52
+ "[#{methods.join(', ')}]"
53
+ end
54
+
55
+ # delegate undefined methods to the @collection array
56
+ # E.g. contact.cases should behave like an array and allow `length`, `size`, `each`, etc.
57
+ def method_missing(method_name, *args, &block)
58
+ @associations.send(method_name.to_sym, *args, &block)
59
+ end
60
+
61
+ end
62
+ end
@@ -2,7 +2,7 @@ module SugarCRM; module AttributeMethods
2
2
 
3
3
  module ClassMethods
4
4
  # Returns a hash of the module fields from the module
5
- def attributes_from_module_fields
5
+ def attributes_from_module
6
6
  fields = {}.with_indifferent_access
7
7
  self._module.fields.keys.sort.each do |k|
8
8
  fields[k] = nil
@@ -74,7 +74,7 @@ module SugarCRM; module AttributeMethods
74
74
  # royally screws up our typecasting code, so we handle it here.
75
75
  def merge_attributes(attrs={})
76
76
  # copy attributes from the parent module fields array
77
- @attributes = self.class.attributes_from_module_fields
77
+ @attributes = self.class.attributes_from_module
78
78
  # populate the attributes with values from the attrs provided to init.
79
79
  @attributes.keys.each do |name|
80
80
  write_attribute name, attrs[name] if attrs[name]
@@ -125,7 +125,7 @@ module SugarCRM; module AttributeMethods
125
125
 
126
126
  # Wrapper for invoking save on modified_attributes
127
127
  # sets the id if it's a new record
128
- def save_modified_attributes
128
+ def save_modified_attributes!
129
129
  # Complain if we aren't valid
130
130
  raise InvalidRecord, errors.to_a.join(", ") if !valid?
131
131
  # Send the save request
data/lib/sugarcrm/base.rb CHANGED
@@ -5,7 +5,7 @@ module SugarCRM; class Base
5
5
 
6
6
  # This holds our connection
7
7
  cattr_accessor :connection, :instance_writer => false
8
-
8
+
9
9
  # Tracks if we have extended our class with attribute methods yet.
10
10
  class_attribute :attribute_methods_generated
11
11
  self.attribute_methods_generated = false
@@ -230,11 +230,7 @@ module SugarCRM; class Base
230
230
  record = find(:first, options)
231
231
 
232
232
  if record.nil?
233
- record = self.new do |r|
234
- r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
235
- r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
236
- end
237
- #{'yield(record) if block_given?'}
233
+ record = self.new(unprotected_attributes_for_create)
238
234
  #{'record.save' if instantiator == :create}
239
235
  record
240
236
  else
@@ -250,7 +246,7 @@ module SugarCRM; class Base
250
246
  end
251
247
 
252
248
  def all_attributes_exists?(attribute_names)
253
- attribute_names.all? { |name| attributes_from_module_fields.include?(name) }
249
+ attribute_names.all? { |name| attributes_from_module.include?(name) }
254
250
  end
255
251
 
256
252
  def construct_attributes_from_arguments(attribute_names, arguments)
@@ -269,11 +265,10 @@ module SugarCRM; class Base
269
265
  end
270
266
 
271
267
  # Creates an instance of a Module Class, i.e. Account, User, Contact, etc.
272
- def initialize(attributes={})
268
+ def initialize(attributes={}, &block)
273
269
  @modified_attributes = {}
274
270
  merge_attributes(attributes.with_indifferent_access)
275
271
  clear_association_cache
276
- @associations = self.class.associations_from_module_link_fields
277
272
  define_attribute_methods
278
273
  define_association_methods
279
274
  typecast_attributes
@@ -308,8 +303,8 @@ module SugarCRM; class Base
308
303
  # Saves the current object, and any modified associations.
309
304
  # Raises an exceptions if save fails for any reason.
310
305
  def save!
311
- save_modified_attributes
312
- save_modified_associations
306
+ save_modified_attributes!
307
+ save_modified_associations!
313
308
  true
314
309
  end
315
310
 
@@ -371,6 +366,7 @@ module SugarCRM; class Base
371
366
  include AttributeSerializers
372
367
  include AssociationMethods
373
368
  extend AssociationMethods::ClassMethods
369
+ include AssociationCache
374
370
  end
375
371
 
376
372
  end; end
@@ -1,23 +1,23 @@
1
1
  module SugarCRM; class Connection
2
- # Retrieve a list of SugarBeans by ID. This method will not
3
- # work with the report module.
4
- def get_entries(module_name, ids, opts={})
5
- login! unless logged_in?
6
- options = {
7
- :fields => [],
8
- :link_fields => [],
9
- }.merge! opts
2
+ # Retrieve a list of SugarBeans by ID. This method will not
3
+ # work with the report module.
4
+ def get_entries(module_name, ids, opts={})
5
+ login! unless logged_in?
6
+ options = {
7
+ :fields => [],
8
+ :link_fields => [],
9
+ }.merge! opts
10
10
 
11
- json = <<-EOF
12
- {
13
- \"session\": \"#{@session}\"\,
14
- \"module_name\": \"#{module_name}\"\,
15
- \"ids\": #{ids.to_json}\,
16
- \"select_fields\": #{resolve_fields(module_name, options[:fields])}\,
17
- \"link_name_to_fields_array\": #{options[:link_fields].to_json}\,
18
- }
19
- EOF
20
- json.gsub!(/^\s{6}/,'')
21
- SugarCRM::Response.handle(send!(:get_entries, json))
22
- end
11
+ json = <<-EOF
12
+ {
13
+ \"session\": \"#{@session}\"\,
14
+ \"module_name\": \"#{module_name}\"\,
15
+ \"ids\": #{ids.to_json}\,
16
+ \"select_fields\": #{resolve_fields(module_name, options[:fields])}\,
17
+ \"link_name_to_fields_array\": #{options[:link_fields].to_json}\,
18
+ }
19
+ EOF
20
+ json.gsub!(/^\s{6}/,'')
21
+ SugarCRM::Response.handle(send!(:get_entries, json))
22
+ end
23
23
  end; end
@@ -1,20 +1,20 @@
1
1
  module SugarCRM; class Connection
2
- # Retrieves the specified number of records in a module.
3
- def get_entries_count(module_name, query, opts={})
4
- login! unless logged_in?
5
- options = {
6
- :deleted => 0
7
- }.merge! opts
2
+ # Retrieves the specified number of records in a module.
3
+ def get_entries_count(module_name, query, opts={})
4
+ login! unless logged_in?
5
+ options = {
6
+ :deleted => 0
7
+ }.merge! opts
8
8
 
9
- json = <<-EOF
10
- {
11
- \"session\": \"#{@session}\"\,
12
- \"module_name\": \"#{module_name}\"\,
13
- \"query\": \"#{query}\"\,
14
- \"deleted\": #{options[:deleted]}
15
- }
16
- EOF
17
- json.gsub!(/^\s{6}/,'')
18
- send!(:get_entries_count, json)
19
- end
9
+ json = <<-EOF
10
+ {
11
+ \"session\": \"#{@session}\"\,
12
+ \"module_name\": \"#{module_name}\"\,
13
+ \"query\": \"#{query}\"\,
14
+ \"deleted\": #{options[:deleted]}
15
+ }
16
+ EOF
17
+ json.gsub!(/^\s{6}/,'')
18
+ send!(:get_entries_count, json)
19
+ end
20
20
  end; end
@@ -1,31 +1,31 @@
1
1
  module SugarCRM; class Connection
2
- # Retrieve a list of SugarBeans. This is the primary method for getting
3
- # a list of SugarBeans using the REST API.
4
- def get_entry_list(module_name, query, opts={})
5
- login! unless logged_in?
6
- options = {
7
- :order_by => '',
8
- :offset => '',
9
- :fields => [],
10
- :link_fields => [],
11
- :limit => '',
12
- :deleted => 0
13
- }.merge! opts
2
+ # Retrieve a list of SugarBeans. This is the primary method for getting
3
+ # a list of SugarBeans using the REST API.
4
+ def get_entry_list(module_name, query, opts={})
5
+ login! unless logged_in?
6
+ options = {
7
+ :order_by => '',
8
+ :offset => '',
9
+ :fields => [],
10
+ :link_fields => [],
11
+ :limit => '',
12
+ :deleted => 0
13
+ }.merge! opts
14
14
 
15
- json = <<-EOF
16
- {
17
- \"session\": \"#{@session}\"\,
18
- \"module_name\": \"#{module_name}\"\,
19
- \"query\": \"#{query}\"\,
20
- \"order_by\": \"#{options[:order_by]}\"\,
21
- \"offset\": \"#{options[:offset]}\"\,
22
- \"select_fields\": #{resolve_fields(module_name, options[:fields])}\,
23
- \"link_name_to_fields_array\": #{options[:link_fields].to_json}\,
24
- \"max_results\": \"#{options[:limit]}\"\,
25
- \"deleted\": #{options[:deleted]}
26
- }
27
- EOF
28
- json.gsub!(/^\s{6}/,'')
29
- SugarCRM::Response.handle(send!(:get_entry_list, json))
30
- end
15
+ json = <<-EOF
16
+ {
17
+ \"session\": \"#{@session}\"\,
18
+ \"module_name\": \"#{module_name}\"\,
19
+ \"query\": \"#{query}\"\,
20
+ \"order_by\": \"#{options[:order_by]}\"\,
21
+ \"offset\": \"#{options[:offset]}\"\,
22
+ \"select_fields\": #{resolve_fields(module_name, options[:fields])}\,
23
+ \"link_name_to_fields_array\": #{options[:link_fields].to_json}\,
24
+ \"max_results\": \"#{options[:limit]}\"\,
25
+ \"deleted\": #{options[:deleted]}
26
+ }
27
+ EOF
28
+ json.gsub!(/^\s{6}/,'')
29
+ SugarCRM::Response.handle(send!(:get_entry_list, json))
30
+ end
31
31
  end; end
@@ -1,19 +1,15 @@
1
1
  module SugarCRM; class Connection
2
-
3
- # Retrieves the vardef information on fields of the specified bean.
4
- def get_module_fields(module_name)
5
- login! unless logged_in?
6
- json = <<-"EOF"
7
- {
8
- \"session\": \"#{@session}\"\,
9
- \"module_name": \"#{module_name}"
10
- }
11
- EOF
12
- json.gsub!(/^\s{6}/,'')
13
- SugarCRM::Response.handle(send!(:get_module_fields, json))
14
- #send!(:get_module_fields, json)
15
- end
16
-
17
- alias :get_fields :get_module_fields
18
-
2
+ # Retrieves the vardef information of the specified bean.
3
+ def get_module_fields(module_name)
4
+ login! unless logged_in?
5
+ json = <<-EOF
6
+ {
7
+ \"session\": \"#{@session}\"\,
8
+ \"module_name": \"#{module_name}"
9
+ }
10
+ EOF
11
+ json.gsub!(/^\s{6}/,'')
12
+ SugarCRM::Response.handle(send!(:get_module_fields, json))
13
+ end
14
+ alias :get_fields :get_module_fields
19
15
  end; end
@@ -1,14 +1,14 @@
1
1
  module SugarCRM; class Connection
2
- # Retrieves an attachment from a note.
3
- def get_note_attachment(id)
4
- login! unless logged_in?
5
- json = <<-EOF
6
- {
7
- \"session\": \"#{@session}\"\,
8
- \"id\": #{id}\
9
- }
10
- EOF
11
- json.gsub!(/^\s{6}/,'')
12
- send!(:get_note_attachment, json)
13
- end
2
+ # Retrieves an attachment from a note.
3
+ def get_note_attachment(id)
4
+ login! unless logged_in?
5
+ json = <<-EOF
6
+ {
7
+ \"session\": \"#{@session}\"\,
8
+ \"id\": #{id}\
9
+ }
10
+ EOF
11
+ json.gsub!(/^\s{6}/,'')
12
+ send!(:get_note_attachment, json)
13
+ end
14
14
  end; end
@@ -1,34 +1,30 @@
1
1
  module SugarCRM; class Connection
2
- # Retrieves a collection of beans that are related
3
- # to the specified bean and, optionally, returns
4
- # relationship data
5
- def get_relationships(module_name, id, related_to, opts={})
6
- login! unless logged_in?
7
- options = {
8
- :query => '',
9
- :fields => [],
10
- :link_fields => [],
11
- :deleted => ''
12
- }.merge! opts
13
-
14
- related_module = related_to.classify
15
-
16
- json = <<-EOF
17
- {
18
- \"session\": \"#{@session}\"\,
19
- \"module_name\": \"#{module_name}\"\,
20
- \"module_id\": \"#{id}\"\,
21
- \"link_field_name\": \"#{related_to.downcase}\"\,
22
- \"related_module_query\": \"#{options[:query]}\"\,
23
- \"related_fields\": #{resolve_fields(related_module, options[:fields])}\,
24
- \"related_module_link_name_to_fields_array\": #{options[:link_fields].to_json}\,
25
- \"deleted\": #{options[:deleted]}
26
- }
27
- EOF
28
- json.gsub!(/^\s{6}/,'')
29
- SugarCRM::Response.new(send!(:get_relationships, json)).to_obj
30
- end
31
-
32
- alias :get_relationship :get_relationships
2
+ # Retrieves a collection of beans that are related
3
+ # to the specified bean and, optionally, returns
4
+ # relationship data
5
+ def get_relationships(module_name, id, related_to, opts={})
6
+ login! unless logged_in?
7
+ options = {
8
+ :query => '',
9
+ :fields => [],
10
+ :link_fields => [],
11
+ :deleted => ''
12
+ }.merge! opts
33
13
 
14
+ json = <<-EOF
15
+ {
16
+ \"session\": \"#{@session}\"\,
17
+ \"module_name\": \"#{module_name}\"\,
18
+ \"module_id\": \"#{id}\"\,
19
+ \"link_field_name\": \"#{related_to.downcase}\"\,
20
+ \"related_module_query\": \"#{options[:query]}\"\,
21
+ \"related_fields\": #{resolve_related_fields(module_name, related_to)}\,
22
+ \"related_module_link_name_to_fields_array\": #{options[:link_fields].to_json}\,
23
+ \"deleted\": #{options[:deleted]}
24
+ }
25
+ EOF
26
+ json.gsub!(/^\s{6}/,'')
27
+ SugarCRM::Response.new(send!(:get_relationships, json), {:always_return_array => true}).to_obj
28
+ end
29
+ alias :get_relationship :get_relationships
34
30
  end; end
@@ -1,17 +1,17 @@
1
1
  module SugarCRM; class Connection
2
- # Retrieves a list of report entries based on specified report IDs.
3
- def get_report_entries(ids, opts={})
4
- login! unless logged_in?
5
- options = {:select_fields => ''}.merge! opts
2
+ # Retrieves a list of report entries based on specified report IDs.
3
+ def get_report_entries(ids, opts={})
4
+ login! unless logged_in?
5
+ options = {:select_fields => ''}.merge! opts
6
6
 
7
- json = <<-EOF
8
- {
9
- \"session\": \"#{@session}\"\,
10
- \"ids\": #{ids.to_json}\,
11
- \"select_fields\": \"#{options[:select_fields].to_json}\"
12
- }
13
- EOF
14
- json.gsub!(/^\s{6}/,'')
15
- send!(:get_report_entries, json)
16
- end
7
+ json = <<-EOF
8
+ {
9
+ \"session\": \"#{@session}\"\,
10
+ \"ids\": #{ids.to_json}\,
11
+ \"select_fields\": \"#{options[:select_fields].to_json}\"
12
+ }
13
+ EOF
14
+ json.gsub!(/^\s{6}/,'')
15
+ send!(:get_report_entries, json)
16
+ end
17
17
  end; end
@@ -12,7 +12,7 @@ module SugarCRM; class Connection
12
12
  \"application\": \"\"
13
13
  }
14
14
  EOF
15
- json.gsub!(/^\s{8}/,'')
15
+ json.gsub!(/^\s{6}/,'')
16
16
  response = send!(:login, json)
17
17
  end
18
18
  end; end
@@ -9,7 +9,7 @@ module SugarCRM; class Connection
9
9
  }
10
10
  }
11
11
  EOF
12
- json.gsub!(/^\s{8}/,'')
12
+ json.gsub!(/^\s{6}/,'')
13
13
  send!(:logout, json)
14
14
  end
15
15
  end; end
@@ -7,7 +7,7 @@ module SugarCRM; class Connection
7
7
  \"session\": \"#{@session}\"
8
8
  }
9
9
  EOF
10
- json.gsub!(/^\s{8}/,'')
10
+ json.gsub!(/^\s{6}/,'')
11
11
  response = send!(:seamless_login, json)
12
12
  end
13
13
  end; end
@@ -1,15 +1,40 @@
1
1
  module SugarCRM; class Connection
2
+ # Attempts to return a list of fields for the target of the association.
3
+ # i.e. if we are associating Contact -> Account, using the "contacts" link
4
+ # field name - this will lookup the contacts association and try to determine
5
+ # the target object type (Contact). It will then pull the fields for that object
6
+ # and shove them in the related_fields portion of the get_relationship request.
7
+ def resolve_related_fields(module_name, link_field)
8
+ a = Association.new(class_for(module_name), link_field)
9
+ if a.target
10
+ fields = a.target.new.attributes.keys
11
+ else
12
+ fields = ["id"]
13
+ end
14
+ fields.to_json
15
+ end
16
+
2
17
  def resolve_fields(module_name, fields)
3
18
  # FIXME: This is to work around a bug in SugarCRM 6.0
4
19
  # where no fields are returned if no fields are specified
5
20
  if fields.length == 0
6
- mod = Module.find(module_name)
21
+ mod = Module.find(module_name.classify)
7
22
  if mod
8
23
  fields = mod.fields.keys
9
- else
10
- fields = "id"
24
+ else
25
+ fields = ["id"]
11
26
  end
12
27
  end
13
28
  return fields.to_json
14
29
  end
30
+
31
+ # Returns an instance of class for the provided module name
32
+ def class_for(module_name)
33
+ begin
34
+ klass = "SugarCRM::#{module_name.classify}".constantize.new
35
+ rescue NameError
36
+ raise InvalidModule, "Module: #{module_name} is not registered"
37
+ end
38
+ end
39
+
15
40
  end; end
@@ -15,7 +15,7 @@ module SugarCRM; class Request
15
15
  @request << '&rest_data=' << @json
16
16
  if debug
17
17
  puts "#{method}: Request:"
18
- pp @request
18
+ puts @request
19
19
  puts "\n"
20
20
  end
21
21
  self
@@ -26,7 +26,8 @@ module SugarCRM; class Response
26
26
 
27
27
  attr :response, false
28
28
 
29
- def initialize(json)
29
+ def initialize(json,opts={})
30
+ @options = { :always_return_array => false }.merge! opts
30
31
  @response = json
31
32
  @response = json.with_indifferent_access if json.is_a? Hash
32
33
  end
@@ -52,7 +53,7 @@ module SugarCRM; class Response
52
53
  end
53
54
  end
54
55
  # If we only have one result, just return the object
55
- if objects.length == 1
56
+ if objects.length == 1 && !@options[:always_return_array]
56
57
  return objects[0]
57
58
  else
58
59
  return objects
@@ -13,4 +13,5 @@ module SugarCRM
13
13
  class UninitializedModule < RuntimeError; end
14
14
  class InvalidAttribute < RuntimeError; end
15
15
  class InvalidAttributeType < RuntimeError; end
16
+ class InvalidAssociation < RuntimeError; end
16
17
  end
@@ -7,6 +7,7 @@ module SugarCRM
7
7
  attr :klass, true
8
8
  attr :fields, true
9
9
  attr :link_fields, true
10
+ alias :bean :klass
10
11
 
11
12
  # Dynamically register objects based on Module name
12
13
  # I.e. a SugarCRM Module named Users will generate
@@ -15,27 +16,28 @@ module SugarCRM
15
16
  @name = name
16
17
  @klass = name.classify
17
18
  @table_name = name.tableize
18
-
19
- # set table name for custom attibutes
20
- # custom attributes are contained in a table named after the module, with a '_cstm' suffix
21
- # the module's table name must be tableized for the modules that ship with SugarCRM
22
- # for custom modules (created in the Studio), table name don't need to be tableized: the name passed to the constructor is already tableized
23
- unless self.custom_module?
24
- @custom_table_name = @table_name + "_cstm"
25
- else
26
- @custom_table_name = name + "_cstm"
27
- end
28
-
19
+ @custom_table_name = resolve_custom_table_name
29
20
  @fields = {}
30
21
  @link_fields = {}
31
22
  @fields_registered = false
32
23
  self
33
24
  end
34
25
 
35
- # return true if this module was created in the SugarCRM Studio (i.e. it is not part of the modules that
36
- # ship in the dfault SugarCRM configuration
26
+ # Return true if this module was created in the SugarCRM Studio (i.e. it is not part of the modules that
27
+ # ship in the default SugarCRM configuration)
37
28
  def custom_module?
38
- name.downcase == name # custom module names are all lower_case, whereas SugarCRM modules are CamelCase
29
+ # custom module names are all lower_case, whereas SugarCRM modules are CamelCase
30
+ @name.downcase == @name
31
+ end
32
+
33
+ # Set table name for custom attibutes
34
+ # Custom attributes are contained in a table named after the module, with a '_cstm' suffix.
35
+ # The module's table name must be tableized for the modules that ship with SugarCRM.
36
+ # For custom modules (created in the Studio), table name don't need to be tableized since
37
+ # the name passed to the constructor is already tableized
38
+ def resolve_custom_table_name
39
+ @custom_table_name = @table_name + "_cstm"
40
+ @custom_table_name = @name + "_cstm" if custom_module?
39
41
  end
40
42
 
41
43
  # Returns the fields associated with the module
@@ -6,10 +6,10 @@ class TestGetRelationships < Test::Unit::TestCase
6
6
  SugarCRM::Connection.new(URL, USER, PASS, {:register_modules => false, :debug => false})
7
7
  end
8
8
  should "return a list of email_addresses when sent #get_relationship and a user_id" do
9
- email_address = SugarCRM.connection.get_relationships(
9
+ email_addresses = SugarCRM.connection.get_relationships(
10
10
  "Users",1,"email_addresses"
11
11
  )
12
- assert_instance_of SugarCRM::EmailAddress, email_address
12
+ assert_instance_of SugarCRM::EmailAddress, email_addresses.first
13
13
  end
14
14
  end
15
15
  end
@@ -0,0 +1 @@
1
+ #TODO: Figure out how to test this.
@@ -31,5 +31,11 @@ class TestAssociations < Test::Unit::TestCase
31
31
  assert u.meetings.save!
32
32
  assert !u.meetings.include?(m)
33
33
  end
34
+
35
+ # TODO: Fix created_by_link to only return a single result
36
+ should "return a user when sent #created_by_link" do
37
+ a = SugarCRM::Account.first
38
+ assert_instance_of SugarCRM::User, a.created_by_link.first
39
+ end
34
40
  end
35
41
  end
data/test/test_module.rb CHANGED
@@ -15,8 +15,9 @@ class TestModule < Test::Unit::TestCase
15
15
  assert SugarCRM::User._module.required_fields.include? :user_name
16
16
  end
17
17
 
18
- should "return the custom table name when #custom_table_name" do
19
- assert_equal "accounts_cstm", SugarCRM::Account._module.custom_table_name
20
- end
18
+ # TODO: Figure out a way to test this.
19
+ #should "return the custom table name when #custom_table_name" do
20
+ # assert_equal "accounts_cstm", SugarCRM::Account._module.custom_table_name
21
+ #end
21
22
  end
22
23
  end
@@ -39,7 +39,7 @@ class TestSugarCRM < Test::Unit::TestCase
39
39
  end
40
40
 
41
41
  should "respond to self.attributes_from_modules_fields" do
42
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, SugarCRM::User.attributes_from_module_fields
42
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, SugarCRM::User.attributes_from_module
43
43
  end
44
44
 
45
45
  should "return an instance of itself when #new" do
@@ -134,6 +134,21 @@ class TestSugarCRM < Test::Unit::TestCase
134
134
  assert_equal "sarah@example.com", u.email_addresses.first.email_address
135
135
  end
136
136
 
137
+ should "create or retrieve a record when #find_or_create_by_name" do
138
+ a = SugarCRM::Account.find_or_create_by_name("Really Important Co. Name")
139
+ assert_instance_of SugarCRM::Account, a
140
+ assert !a.new?
141
+ b = SugarCRM::Account.find_or_create_by_name("Really Important Co. Name")
142
+ assert a == b
143
+ assert a.delete
144
+ end
145
+
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
+
137
152
  end
138
153
 
139
154
  end
metadata CHANGED
@@ -1,13 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sugarcrm
3
3
  version: !ruby/object:Gem::Version
4
- hash: 53
5
- prerelease:
4
+ prerelease: false
6
5
  segments:
7
6
  - 0
8
7
  - 9
9
- - 7
10
- version: 0.9.7
8
+ - 8
9
+ version: 0.9.8
11
10
  platform: ruby
12
11
  authors:
13
12
  - Carl Hicks
@@ -15,99 +14,93 @@ autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
16
 
18
- date: 2011-01-15 00:00:00 -08:00
17
+ date: 2011-01-18 00:00:00 -08:00
19
18
  default_executable:
20
19
  dependencies:
21
20
  - !ruby/object:Gem::Dependency
22
- type: :runtime
23
- version_requirements: &id001 !ruby/object:Gem::Requirement
21
+ name: activesupport
22
+ requirement: &id001 !ruby/object:Gem::Requirement
24
23
  none: false
25
24
  requirements:
26
25
  - - ">="
27
26
  - !ruby/object:Gem::Version
28
- hash: 7
29
27
  segments:
30
28
  - 3
31
29
  - 0
32
30
  - 0
33
31
  version: 3.0.0
34
- requirement: *id001
32
+ type: :runtime
35
33
  prerelease: false
36
- name: activesupport
34
+ version_requirements: *id001
37
35
  - !ruby/object:Gem::Dependency
38
- type: :runtime
39
- version_requirements: &id002 !ruby/object:Gem::Requirement
36
+ name: i18n
37
+ requirement: &id002 !ruby/object:Gem::Requirement
40
38
  none: false
41
39
  requirements:
42
40
  - - ">="
43
41
  - !ruby/object:Gem::Version
44
- hash: 3
45
42
  segments:
46
43
  - 0
47
44
  version: "0"
48
- requirement: *id002
45
+ type: :runtime
49
46
  prerelease: false
50
- name: i18n
47
+ version_requirements: *id002
51
48
  - !ruby/object:Gem::Dependency
52
- type: :development
53
- version_requirements: &id003 !ruby/object:Gem::Requirement
49
+ name: shoulda
50
+ requirement: &id003 !ruby/object:Gem::Requirement
54
51
  none: false
55
52
  requirements:
56
53
  - - ">="
57
54
  - !ruby/object:Gem::Version
58
- hash: 3
59
55
  segments:
60
56
  - 0
61
57
  version: "0"
62
- requirement: *id003
58
+ type: :development
63
59
  prerelease: false
64
- name: shoulda
60
+ version_requirements: *id003
65
61
  - !ruby/object:Gem::Dependency
66
- type: :development
67
- version_requirements: &id004 !ruby/object:Gem::Requirement
62
+ name: bundler
63
+ requirement: &id004 !ruby/object:Gem::Requirement
68
64
  none: false
69
65
  requirements:
70
66
  - - ~>
71
67
  - !ruby/object:Gem::Version
72
- hash: 23
73
68
  segments:
74
69
  - 1
75
70
  - 0
76
71
  - 0
77
72
  version: 1.0.0
78
- requirement: *id004
73
+ type: :development
79
74
  prerelease: false
80
- name: bundler
75
+ version_requirements: *id004
81
76
  - !ruby/object:Gem::Dependency
82
- type: :development
83
- version_requirements: &id005 !ruby/object:Gem::Requirement
77
+ name: jeweler
78
+ requirement: &id005 !ruby/object:Gem::Requirement
84
79
  none: false
85
80
  requirements:
86
81
  - - ~>
87
82
  - !ruby/object:Gem::Version
88
- hash: 7
89
83
  segments:
90
84
  - 1
91
85
  - 5
92
86
  - 2
93
87
  version: 1.5.2
94
- requirement: *id005
88
+ type: :development
95
89
  prerelease: false
96
- name: jeweler
90
+ version_requirements: *id005
97
91
  - !ruby/object:Gem::Dependency
98
- type: :development
99
- version_requirements: &id006 !ruby/object:Gem::Requirement
92
+ name: rcov
93
+ requirement: &id006 !ruby/object:Gem::Requirement
100
94
  none: false
101
95
  requirements:
102
96
  - - ">="
103
97
  - !ruby/object:Gem::Version
104
- hash: 3
105
98
  segments:
106
99
  - 0
107
100
  version: "0"
108
- requirement: *id006
101
+ type: :development
109
102
  prerelease: false
110
- name: rcov
103
+ version_requirements: *id006
111
104
  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.
112
105
  email: carl.hicks@gmail.com
113
106
  executables: []
@@ -126,8 +119,11 @@ files:
126
119
  - VERSION
127
120
  - lib/sugarcrm.rb
128
121
  - lib/sugarcrm/associations.rb
122
+ - lib/sugarcrm/associations/association.rb
123
+ - lib/sugarcrm/associations/association_cache.rb
129
124
  - lib/sugarcrm/associations/association_collection.rb
130
125
  - lib/sugarcrm/associations/association_methods.rb
126
+ - lib/sugarcrm/associations/associations.rb
131
127
  - lib/sugarcrm/attributes.rb
132
128
  - lib/sugarcrm/attributes/attribute_methods.rb
133
129
  - lib/sugarcrm/attributes/attribute_serializers.rb
@@ -180,6 +176,7 @@ files:
180
176
  - test/connection/test_logout.rb
181
177
  - test/connection/test_set_relationship.rb
182
178
  - test/helper.rb
179
+ - test/test_association.rb
183
180
  - test/test_association_collection.rb
184
181
  - test/test_associations.rb
185
182
  - test/test_connection.rb
@@ -200,7 +197,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
200
197
  requirements:
201
198
  - - ">="
202
199
  - !ruby/object:Gem::Version
203
- hash: 3
200
+ hash: -1258950620288984122
204
201
  segments:
205
202
  - 0
206
203
  version: "0"
@@ -209,14 +206,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
209
206
  requirements:
210
207
  - - ">="
211
208
  - !ruby/object:Gem::Version
212
- hash: 3
213
209
  segments:
214
210
  - 0
215
211
  version: "0"
216
212
  requirements: []
217
213
 
218
214
  rubyforge_project:
219
- rubygems_version: 1.4.1
215
+ rubygems_version: 1.3.7
220
216
  signing_key:
221
217
  specification_version: 3
222
218
  summary: Ruby based REST client for SugarCRM
@@ -234,6 +230,7 @@ test_files:
234
230
  - test/connection/test_logout.rb
235
231
  - test/connection/test_set_relationship.rb
236
232
  - test/helper.rb
233
+ - test/test_association.rb
237
234
  - test/test_association_collection.rb
238
235
  - test/test_associations.rb
239
236
  - test/test_connection.rb