sugarcrm 0.8.2 → 0.9.0

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