sugarcrm 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. data/README.rdoc +58 -19
  2. data/Rakefile +1 -4
  3. data/VERSION +1 -1
  4. data/lib/sugarcrm.rb +7 -4
  5. data/lib/sugarcrm/associations.rb +2 -0
  6. data/lib/sugarcrm/associations/association_collection.rb +143 -0
  7. data/lib/sugarcrm/associations/association_methods.rb +92 -0
  8. data/lib/sugarcrm/attributes.rb +4 -0
  9. data/lib/sugarcrm/attributes/attribute_methods.rb +131 -0
  10. data/lib/sugarcrm/attributes/attribute_serializers.rb +55 -0
  11. data/lib/sugarcrm/attributes/attribute_typecast.rb +39 -0
  12. data/lib/sugarcrm/attributes/attribute_validations.rb +37 -0
  13. data/lib/sugarcrm/base.rb +77 -21
  14. data/lib/sugarcrm/connection.rb +3 -137
  15. data/lib/sugarcrm/connection/api/get_entry_list.rb +1 -1
  16. data/lib/sugarcrm/connection/api/get_note_attachment.rb +0 -1
  17. data/lib/sugarcrm/connection/api/set_relationship.rb +3 -0
  18. data/lib/sugarcrm/connection/connection.rb +144 -0
  19. data/lib/sugarcrm/{request.rb → connection/request.rb} +2 -10
  20. data/lib/sugarcrm/{response.rb → connection/response.rb} +10 -4
  21. data/lib/sugarcrm/exceptions.rb +14 -26
  22. data/lib/sugarcrm/module.rb +19 -18
  23. data/test/connection/test_get_entry.rb +5 -5
  24. data/test/connection/test_get_module_fields.rb +1 -1
  25. data/test/connection/test_set_relationship.rb +13 -21
  26. data/test/helper.rb +1 -1
  27. data/test/test_association_collection.rb +12 -0
  28. data/test/test_associations.rb +33 -0
  29. data/test/test_connection.rb +0 -7
  30. data/test/test_module.rb +1 -1
  31. data/test/test_sugarcrm.rb +16 -8
  32. metadata +22 -28
  33. data/lib/sugarcrm/association_methods.rb +0 -46
  34. data/lib/sugarcrm/attribute_methods.rb +0 -170
@@ -12,17 +12,17 @@ RubyGem for interacting with SugarCRM via REST.
12
12
 
13
13
  A less clunky way to interact with SugarCRM via REST.
14
14
 
15
- I've built an abstraction layer on top of the SugarCRM REST API, instead of get_entry("Users", "1") you can
16
- call SugarCRM::User.find(1). There is also support for collections à la SugarCRM::User.find(1).email_addresses.
17
- ActiveRecord style finders are in place, with limited support for conditions and joins
18
- e.g. SugarCRM::Contacts.find_by_title("VP of Sales") will work, but SugarCRM::Contacts.find_by_title("VP of Sales", {:conditions => {:deleted => 0}}) will not.
15
+ Instead of SugarCRM.connection.get_entry("Users", "1"), you can use SugarCRM::User.find(1). There is also support for collections à 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.
19
16
 
20
17
  == FEATURES/PROBLEMS:
21
18
 
22
- * Supports all v2 API calls
23
- * Supports saving of Module specific objects.
24
- * Auto-generation of Module specific objects. When a connection is established, get_available_modules is called and the resultant modules are turned into SugarCRM::Module classes.
25
- * If you just want to use the vanilla API, you can access the methods directly on the SugarCRM.connection object.
19
+ * Works with all v2 API calls
20
+ * Supports creation, saving, and deletion of SugarCRM specific objects.
21
+ * Validations, typecasting, and serialization of boolean, date, and integer fields
22
+ * Query, update and delete records from collections!
23
+ * ActiveRecord style finders!
24
+ * Auto-generation of SugarCRM specific objects. When a connection is established, get_available_modules is called and the resultant modules are turned into SugarCRM::Module classes.
25
+ * If you want to use the vanilla API, you can access the methods directly on the SugarCRM.connection object.
26
26
 
27
27
  == SYNOPSIS:
28
28
 
@@ -50,32 +50,58 @@ e.g. SugarCRM::Contacts.find_by_title("VP of Sales") will work, but SugarCRM::Co
50
50
 
51
51
  # Check if an object is valid (i.e. if it has the required fields to save)
52
52
  u.valid?
53
-
53
+
54
54
  # Access the errors collection
55
55
  u.errors
56
56
 
57
+ # Show the fields required to save
58
+ u.required_attributes
59
+
57
60
  # Delete an Account
58
61
  a = SugarCRM::Account.find_by_name("JAB Funds Ltd.")
59
62
  a.delete
60
63
 
61
- # Retrieve all Email Addresses assigned to a particular user.
64
+ # Retrieve all Email Addresses assigned to a particular User.
62
65
  SugarCRM::User.find_by_user_name('sarah').email_addresses
63
66
 
64
- # Retrieve all email addresses on an Account
67
+ # Retrieve all Email Addresses on an Account
65
68
  SugarCRM::Account.find_by_name("JAB Funds Ltd.").contacts.each do |contact|
66
69
  contact.email_addresses.each do |email|
67
- puts "#{email.email_address}" unless email.opt_out == "1"
70
+ puts "#{email.email_address}" unless email.opt_out == true
68
71
  end
69
72
  end
70
73
 
71
- # Look up the fields for a given module
72
- SugarCRM::Module.find("Accounts").fields
74
+ # Add a Meeting to a Contact
75
+ c = SugarCRM::Contact.first
76
+ c.meetings << SugarCRM::Meeting.new({
77
+ :name => "Product Introduction",
78
+ :date_start => DateTime.now,
79
+ :duration_hours => 1
80
+ })
81
+ c.save!
73
82
 
74
- # Look up the relationships for a given module
75
- SugarCRM::Module.find("Accounts").link_fields
83
+ # Add a Contact to an Account
84
+ a = SugarCRM::Account.find_by_name("JAB Funds Ltd.")
85
+ c = SugarCRM::Contact.new
86
+ c.last_name = 'Doe'
87
+ a.contacts << c
88
+ a.save # or a.contacts.save
76
89
 
77
- # Use the HTTP Connection and SugarCRM API to load the Admin user
78
- SugarCRM.connection.get_entry("Users", 1)
90
+ # Check if an Account has a specific Contact associated with it
91
+ c = SugarCRM::Contact.find_by_last_name("Doe")
92
+ a = SugarCRM::Account.find_by_name("JAB Funds Ltd.")
93
+ a.contacts.include?(c)
94
+
95
+ # Remove a Contact from an Account
96
+ c = SugarCRM::Contact.find_by_last_name("Doe")
97
+ a = SugarCRM::Account.find_by_name("JAB Funds Ltd.")
98
+ a.contacts.delete(c)
99
+ a.save # or a.contacts.save
100
+
101
+ # Look up the Case with the smallest case number
102
+ SugarCRM::Case.first({
103
+ :order_by => 'case_number'
104
+ })
79
105
 
80
106
  # Retrieve the first 10 Accounts with a zip code between 10000 and 10500
81
107
  SugarCRM::Account.all({
@@ -83,6 +109,20 @@ e.g. SugarCRM::Contacts.find_by_title("VP of Sales") will work, but SugarCRM::Co
83
109
  :limit => '10',
84
110
  :order_by => 'billing_address_postalcode'
85
111
  })
112
+
113
+ # Retrieve all Accounts with a zip code
114
+ SugarCRM::Account.all({
115
+ :conditions => { :billing_address_postalcode => "<> NULL" }
116
+ })
117
+
118
+ # Look up the fields for a given module
119
+ SugarCRM::Module.find("Accounts").fields
120
+
121
+ # Look up the relationships for a given module
122
+ SugarCRM::Module.find("Accounts").link_fields
123
+
124
+ # Use the HTTP Connection and SugarCRM API to load the Admin user
125
+ SugarCRM.connection.get_entry("Users", 1)
86
126
 
87
127
  # Retrieve all Accounts by user name (direct API method)
88
128
  SugarCRM.connection.get_entry_list(
@@ -101,7 +141,6 @@ e.g. SugarCRM::Contacts.find_by_title("VP of Sales") will work, but SugarCRM::Co
101
141
  == REQUIREMENTS:
102
142
 
103
143
  * >= activesupport 3.0.0 gem
104
- * json gem
105
144
 
106
145
  == INSTALL:
107
146
 
data/Rakefile CHANGED
@@ -6,14 +6,11 @@ begin
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "sugarcrm"
8
8
  gem.summary = %Q{Ruby based REST client for SugarCRM}
9
- gem.description = %Q{I've implemented all of the basic API calls that SugarCRM supports, and am actively building an abstraction layer
10
- on top of the basic API methods. The end result will be to provide ActiveRecord style finders and first class
11
- objects. Some of this functionality is included today.}
9
+ gem.description = %Q{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 à 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.}
12
10
  gem.email = "carl.hicks@gmail.com"
13
11
  gem.homepage = "http://github.com/chicks/sugarcrm"
14
12
  gem.authors = ["Carl Hicks"]
15
13
  gem.add_development_dependency "shoulda", ">= 0"
16
- gem.add_dependency "json", ">= 0"
17
14
  gem.add_dependency "activesupport", ">= 3.0"
18
15
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
16
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.2
1
+ 0.9.0
@@ -1,13 +1,16 @@
1
+ require 'net/https'
1
2
  require 'pp'
2
3
  require 'set'
4
+ require 'uri'
3
5
  require 'rubygems'
4
6
  require 'active_support/core_ext'
5
7
 
6
8
  require 'sugarcrm/module_methods'
7
- require 'sugarcrm/base'
8
9
  require 'sugarcrm/connection'
9
- require 'sugarcrm/dynamic_finder_match'
10
10
  require 'sugarcrm/exceptions'
11
+ require 'sugarcrm/attributes'
12
+ require 'sugarcrm/associations'
13
+ require 'sugarcrm/dynamic_finder_match'
11
14
  require 'sugarcrm/module'
12
- require 'sugarcrm/request'
13
- require 'sugarcrm/response'
15
+ require 'sugarcrm/base'
16
+
@@ -0,0 +1,2 @@
1
+ require 'sugarcrm/associations/association_methods'
2
+ require 'sugarcrm/associations/association_collection'
@@ -0,0 +1,143 @@
1
+ module SugarCRM
2
+ # A class for handling association collections. Basically just an extension of Array
3
+ # doesn't actually load the records from Sugar until you invoke one of the public methods
4
+ class AssociationCollection
5
+ include Enumerable
6
+
7
+ # creates a new instance of an AssociationCollection
8
+ # Owner is the parent object, and association is the target
9
+ def initialize(owner, association, preload=false)
10
+ @loaded = false
11
+ @owner = owner
12
+ @association= association
13
+ load if preload
14
+ self
15
+ end
16
+
17
+ def changed?
18
+ return false unless loaded?
19
+ return true if added.length > 0
20
+ return true if removed.length > 0
21
+ false
22
+ end
23
+
24
+ def loaded?
25
+ @loaded
26
+ end
27
+
28
+ def load
29
+ load_associated_records unless loaded?
30
+ end
31
+
32
+ def reload
33
+ load_associated_records
34
+ end
35
+
36
+ def each(&block)
37
+ load
38
+ @collection.each(&block)
39
+ end
40
+
41
+ # we should probably delegate this
42
+ def length
43
+ load
44
+ @collection.length
45
+ end
46
+
47
+ # return any added elements
48
+ def added
49
+ load
50
+ @collection - @original
51
+ end
52
+
53
+ # return any removed elements
54
+ def removed
55
+ load
56
+ @original - @collection
57
+ end
58
+
59
+ # Removes an record from the collection, uses the id of the record as a test for inclusion.
60
+ def delete(record)
61
+ load
62
+ raise InvalidRecord, "#{record.class} does not have a valid :id!" if record.id.empty?
63
+ @collection.delete record
64
+ end
65
+
66
+ # Checks if a record is included in the current collection. Uses id's as comparison
67
+ def include?(record)
68
+ load
69
+ @collection.include? record
70
+ end
71
+
72
+ # Add +records+ to this association, saving any unsaved records before adding them.
73
+ # Returns +self+ so method calls may be chained.
74
+ # Be sure to call save on the association to commit any association changes
75
+ def <<(record)
76
+ load
77
+ record.save! if record.new?
78
+ result = true
79
+ result = false if include?(record)
80
+ @collection << record
81
+ result && self
82
+ end
83
+ alias :add :<<
84
+
85
+ def save
86
+ begin
87
+ save!
88
+ rescue
89
+ return false
90
+ end
91
+ end
92
+
93
+ # Pushes collection changes to SugarCRM, and updates the state of the collection
94
+ def save!
95
+ load
96
+ added.each do |record|
97
+ associate!(record)
98
+ end
99
+ removed.each do |record|
100
+ disassociate!(record)
101
+ end
102
+ @original = @collection.dup
103
+ @original.freeze
104
+ true
105
+ end
106
+
107
+ protected
108
+
109
+ # Loads related records for the given association
110
+ def load_associated_records
111
+ array = SugarCRM.connection.get_relationships(@owner.class._module.name, @owner.id, @association.to_s)
112
+ @loaded = true
113
+ # we use original to track the state of the collection at start
114
+ @collection = Array.wrap(array).dup
115
+ @original = Array.wrap(array).freeze
116
+ end
117
+
118
+ # Creates a relationship between the current object and the target
119
+ # Owner is the record the Collection is accessed from
120
+ # Target is the record we are adding to the collection
121
+ # i.e. user.email_addresses.associate!(EmailAddress.new(:email_address => "abc@abc.com"))
122
+ # user would be the owner, and EmailAddress.new() is the target
123
+ def associate!(target, opts={})
124
+ #target.save! if target.new?
125
+ response = SugarCRM.connection.set_relationship(
126
+ @owner.class._module.name, @owner.id,
127
+ target.class._module.table_name, [target.id],
128
+ opts
129
+ )
130
+ raise AssociationFailed,
131
+ "Couldn't associate #{@owner.class._module.name}: #{@owner.id} -> #{target.class._module.table_name}:#{target.id}!" if response["failed"] > 0
132
+ true
133
+ end
134
+
135
+ # Removes a relationship between the current object and the target
136
+ def disassociate!(target)
137
+ associate!(target,{:delete => 1})
138
+ end
139
+
140
+ alias :relate! :associate!
141
+
142
+ end
143
+ end
@@ -0,0 +1,92 @@
1
+ module SugarCRM; module AssociationMethods
2
+
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
+ end
15
+
16
+ def associations_changed?
17
+ @association_cache.values.each do |collection|
18
+ return true if collection.changed?
19
+ end
20
+ false
21
+ end
22
+
23
+ protected
24
+
25
+ def save_modified_associations
26
+ @association_cache.values.each do |collection|
27
+ if collection.changed?
28
+ return false unless collection.save
29
+ end
30
+ end
31
+ true
32
+ end
33
+
34
+ def clear_association_cache
35
+ @association_cache = {}
36
+ end
37
+
38
+ # Generates the association proxy methods for related modules
39
+ def define_association_methods
40
+ return if association_methods_generated?
41
+ @associations.each do |k|
42
+ self.class.module_eval %Q?
43
+ def #{k}
44
+ query_association :#{k}
45
+ end
46
+ ?
47
+ #seed_association_cache(k.to_syn)
48
+ end
49
+ self.class.association_methods_generated = true
50
+ end
51
+
52
+ # def seed_association_cache(association)
53
+ # @association_cache[association] = AssociationCollection.new(self,association)
54
+ # end
55
+
56
+ # Returns the records from the associated module or returns the cached copy if we've already
57
+ # loaded it. Force a reload of the records with reload=true
58
+ #
59
+ # {"email_addresses"=>
60
+ # {"name"=>"email_addresses",
61
+ # "module"=>"EmailAddress",
62
+ # "bean_name"=>"EmailAddress",
63
+ # "relationship"=>"users_email_addresses",
64
+ # "type"=>"link"},
65
+ #
66
+ def query_association(assoc, reload=false)
67
+ association = assoc.to_sym
68
+ return @association_cache[association] if association_cached?(association) && !reload
69
+ # TODO: Some relationships aren't fetchable via get_relationship (i.e users.contacts)
70
+ # even though get_module_fields lists them on the related_fields array. This is most
71
+ # commonly seen with one-to-many relationships without a join table. We need to cook
72
+ # up some elegant way to handle this.
73
+ collection = AssociationCollection.new(self,association,true)
74
+ # add it to the cache
75
+ @association_cache[association] = collection
76
+ collection
77
+ end
78
+
79
+ # Loads related records for the given association
80
+ # def load_associations_for(association)
81
+ # SugarCRM.connection.get_relationships(self.class._module.name, self.id, association.to_s)
82
+ # end
83
+
84
+ # pushes an element to the association collection
85
+ def append_to_association(association, record)
86
+ collection = query_association(association)
87
+ collection << record
88
+ collection
89
+ end
90
+
91
+
92
+ end; end
@@ -0,0 +1,4 @@
1
+ require 'sugarcrm/attributes/attribute_methods'
2
+ require 'sugarcrm/attributes/attribute_validations'
3
+ require 'sugarcrm/attributes/attribute_serializers'
4
+ require 'sugarcrm/attributes/attribute_typecast'
@@ -0,0 +1,131 @@
1
+ module SugarCRM; module AttributeMethods
2
+
3
+ module ClassMethods
4
+ # Returns a hash of the module fields from the module
5
+ # merges matching keys if another attributes hash is provided
6
+ def attributes_from_module_fields
7
+ fields = {}.with_indifferent_access
8
+ self._module.fields.keys.sort.each do |k|
9
+ fields[k] = nil
10
+ end
11
+ fields
12
+ end
13
+ end
14
+
15
+ # TODO: Object.id is not being updated properly. Figure out why...
16
+ alias :pk :id
17
+ alias :primary_key :id
18
+
19
+ # Determines if attributes or associations have been changed
20
+ def changed?
21
+ return true if attributes_changed?
22
+ return true if associations_changed?
23
+ false
24
+ end
25
+
26
+ def attributes_changed?
27
+ @modified_attributes.length > 0
28
+ end
29
+
30
+ # Is this a new record?
31
+ def new?
32
+ @attributes[:id].blank?
33
+ end
34
+
35
+ # List the required attributes for save
36
+ def required_attributes
37
+ self.class._module.required_fields
38
+ end
39
+
40
+ protected
41
+
42
+ # Merges attributes provided as an argument to initialize
43
+ # with attributes from the module.fields array. Skips any
44
+ # fields that aren't in the module.fields array
45
+ #
46
+ # BUG: SugarCRM likes to return fields you don't ask for, and
47
+ # aren't fields on a module (i.e. modified_user_name). This
48
+ # royally screws up our typecasting code, so we handle it here.
49
+ def merge_attributes(attrs={})
50
+ # copy attributes from the parent module fields array
51
+ @attributes = self.class.attributes_from_module_fields
52
+ # populate the attributes with values from the attrs provided to init.
53
+ @attributes.keys.each do |name|
54
+ write_attribute name, attrs[name] if attrs[name]
55
+ end
56
+ # If this is an existing record, blank out the modified_attributes hash
57
+ @modified_attributes = {} unless new?
58
+ end
59
+
60
+ # Generates get/set methods for keys in the attributes hash
61
+ def define_attribute_methods
62
+ return if attribute_methods_generated?
63
+ @attributes.keys.sort.each do |k|
64
+ self.class.module_eval %Q?
65
+ def #{k}
66
+ read_attribute :#{k}
67
+ end
68
+ def #{k}=(value)
69
+ write_attribute :#{k},value
70
+ end
71
+ ?
72
+ end
73
+ self.class.attribute_methods_generated = true
74
+ end
75
+
76
+ # Returns an <tt>#inspect</tt>-like string for the value of the
77
+ # attribute +attr_name+. String attributes are elided after 50
78
+ # characters, and Date and Time attributes are returned in the
79
+ # <tt>:db</tt> format. Other attributes return the value of
80
+ # <tt>#inspect</tt> without modification.
81
+ #
82
+ # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
83
+ #
84
+ # person.attribute_for_inspect(:name)
85
+ # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
86
+ #
87
+ # person.attribute_for_inspect(:created_at)
88
+ # # => '"2009-01-12 04:48:57"'
89
+ def attribute_for_inspect(attr_name)
90
+ value = read_attribute(attr_name)
91
+ if value.is_a?(String) && value.length > 50
92
+ "#{value[0..50]}...".inspect
93
+ elsif value.is_a?(Date) || value.is_a?(Time)
94
+ %("#{value.to_s(:db)}")
95
+ else
96
+ value.inspect
97
+ end
98
+ end
99
+
100
+ # Wrapper for invoking save on modified_attributes
101
+ # sets the id if it's a new record
102
+ def save_modified_attributes
103
+ # Complain if we aren't valid
104
+ raise InvalidRecord, errors.to_a.join(", ") if !valid?
105
+ # Send the save request
106
+ response = SugarCRM.connection.set_entry(self.class._module.name, serialize_modified_attributes)
107
+ # Complain if we don't get a parseable response back
108
+ raise RecordsaveFailed, "Failed to save record: #{self}. Response was not a Hash" unless response.is_a? Hash
109
+ # Complain if we don't get a valid id back
110
+ raise RecordSaveFailed, "Failed to save record: #{self}. Response did not contain a valid 'id'." if response["id"].nil?
111
+ # Save the id to the record, if it's a new record
112
+ @attributes[:id] = response["id"] if new?
113
+ raise InvalidRecord, "Failed to update id for: #{self}." if id.nil?
114
+ # Clear the modified attributes Hash
115
+ @modified_attributes = {}
116
+ true
117
+ end
118
+
119
+ # Wrapper around attributes hash
120
+ def read_attribute(key)
121
+ @attributes[key]
122
+ end
123
+
124
+ # Wrapper around attributes hash
125
+ def write_attribute(key, value)
126
+ @modified_attributes[key] = { :old => @attributes[key].to_s, :new => value }
127
+ @attributes[key] = value
128
+ end
129
+
130
+ end; end
131
+