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
@@ -0,0 +1,55 @@
1
+ module SugarCRM; module AttributeSerializers
2
+
3
+ protected
4
+
5
+ # Serializes the id
6
+ def serialize_id
7
+ {:name => "id", :value => id.to_s}
8
+ end
9
+
10
+ # Converts the attributes hash into format recognizable by Sugar
11
+ # { :last_name => "Smith"}
12
+ # becomes
13
+ # { :last_name => {:name => "last_name", :value => "Smith"}}
14
+ def serialize_attributes
15
+ attr_hash = {}
16
+ @attributes.each_pair do |name,value|
17
+ attr_hash[name] = serialize_attribute(name,value)
18
+ end
19
+ attr_hash[:id] = serialize_id unless new?
20
+ attr_hash
21
+ end
22
+
23
+ # Converts the modified_attributes hash into format recognizable by Sugar
24
+ # { :last_name => {:old => "Smit", :new => "Smith"}}
25
+ # becomes
26
+ # { :last_name => {:name => "last_name", :value => "Smith"}}
27
+ def serialize_modified_attributes
28
+ attr_hash = {}
29
+ @modified_attributes.each_pair do |name,hash|
30
+ attr_hash[name] = serialize_attribute(name,hash[:new])
31
+ end
32
+ attr_hash[:id] = serialize_id unless new?
33
+ attr_hash
34
+ end
35
+
36
+ # Un-typecasts the attribute - false becomes 0
37
+ def serialize_attribute(name,value)
38
+ attr_value = value
39
+ attr_type = attr_type_for(name)
40
+ case attr_type
41
+ when :bool
42
+ attr_value = 0
43
+ attr_value = 1 if value
44
+ when :datetime, :datetimecombo
45
+ begin
46
+ attr_value = value.strftime("%Y-%m-%d %H:%M:%S")
47
+ rescue
48
+ attr_value = value
49
+ end
50
+ when :int
51
+ attr_value = value.to_s
52
+ end
53
+ {:name => name, :value => attr_value}
54
+ end
55
+ end; end
@@ -0,0 +1,39 @@
1
+ module SugarCRM; module AttributeTypeCast
2
+
3
+ protected
4
+
5
+ # Returns the attribute type for a given attribute
6
+ def attr_type_for(attribute)
7
+ # sometimes the module fields aren't loaded. Why?
8
+ fields = self.class._module.fields
9
+ field = fields[attribute]
10
+ raise UninitializedModule, "SugarCRM::Module #{self.class._module.name} was not initialized properly (fields.length == 0)" if fields.length == 0
11
+ raise InvalidAttribute, "#{self.class}_module.fields does not contain an entry for #{attribute} (of type: #{attribute.class})\nValid fields: #{self.class._module.fields.keys.sort.join(", ")}" if field.nil?
12
+ raise InvalidAttributeType, "#{self.class}._module.fields[#{attribute}] does not have a key for \'type\'" if field["type"].nil?
13
+ #return :string unless field.is_a? Hash
14
+ field["type"].to_sym
15
+ end
16
+
17
+ # Attempts to typecast each attribute based on the module field type
18
+ def typecast_attributes
19
+ @attributes.each_pair do |name,value|
20
+ # skip primary key columns
21
+ next if name == "id"
22
+ attr_type = attr_type_for(name)
23
+ case attr_type
24
+ when :bool
25
+ @attributes[name] = (value == "1")
26
+ when :datetime, :datetimecombo
27
+ begin
28
+ @attributes[name] = DateTime.parse(value)
29
+ rescue
30
+ @attributes[name] = value
31
+ end
32
+ when :int
33
+ @attributes[name] = value.to_i
34
+ end
35
+ end
36
+ @attributes
37
+ end
38
+
39
+ end; end
@@ -0,0 +1,37 @@
1
+ module SugarCRM; module AttributeValidations
2
+ # Checks to see if we have all the neccessary attributes
3
+ def valid?
4
+ @errors = Set.new
5
+ self.class._module.required_fields.each do |attribute|
6
+ valid_attribute?(attribute)
7
+ end
8
+ @errors.length == 0
9
+ end
10
+
11
+ protected
12
+
13
+ # TODO: Add test cases for validations
14
+ def valid_attribute?(attribute)
15
+ case attr_type_for(attribute)
16
+ when :bool
17
+ validate_class_for(attribute, [TrueClass, FalseClass])
18
+ when :datetime, :datetimecombo
19
+ validate_class_for(attribute, [DateTime])
20
+ when :int
21
+ validate_class_for(attribute, [Fixnum, Float])
22
+ else
23
+ if @attributes[attribute].blank?
24
+ @errors.add "#{attribute} cannot be blank"
25
+ end
26
+ end
27
+ end
28
+
29
+ # Compares the class of the attribute with the class or classes provided in the class array
30
+ # returns true if they match, otherwise adds an entry to the @errors collection, and returns false
31
+ def validate_class_for(attribute, class_array)
32
+ return true if class_array.include? @attributes[attribute].class
33
+ @errors.add "#{attribute} must be a #{class_array.join(" or ")} object (not #{@attributes[attribute].class})"
34
+ false
35
+ end
36
+
37
+ end; end
@@ -1,10 +1,7 @@
1
- require 'sugarcrm/attribute_methods'
2
- require 'sugarcrm/association_methods'
3
-
4
1
  module SugarCRM; class Base
5
2
 
6
3
  # Unset all of the instance methods we don't need.
7
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$|^define_method$|^class$|^methods$|^instance_of.$|^respond_to.$)/ }
4
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$|^define_method$|^class$|^nil.$|^methods$|^instance_of.$|^respond_to.$)/ }
8
5
 
9
6
  # This holds our connection
10
7
  cattr_accessor :connection, :instance_writer => false
@@ -23,7 +20,6 @@ module SugarCRM; class Base
23
20
  attr :attributes, true
24
21
  attr :modified_attributes, true
25
22
  attr :associations, true
26
- attr :id, true
27
23
  attr :debug, true
28
24
  attr :errors, true
29
25
 
@@ -59,12 +55,44 @@ module SugarCRM; class Base
59
55
  def all(*args)
60
56
  find(:all, *args)
61
57
  end
58
+
59
+ # Creates an object (or multiple objects) and saves it to SugarCRM, if validations pass.
60
+ # The resulting object is returned whether the object was saved successfully to the database or not.
61
+ #
62
+ # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
63
+ # attributes on the objects that are to be created.
64
+ #
65
+ # ==== Examples
66
+ # # Create a single new object
67
+ # User.create(:first_name => 'Jamie')
68
+ #
69
+ # # Create an Array of new objects
70
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
71
+ #
72
+ # # Create a single object and pass it into a block to set other attributes.
73
+ # User.create(:first_name => 'Jamie') do |u|
74
+ # u.is_admin = false
75
+ # end
76
+ #
77
+ # # Creating an Array of new objects using a block, where the block is executed for each object:
78
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
79
+ # u.is_admin = false
80
+ # end
81
+ def create(attributes = nil, &block)
82
+ if attributes.is_a?(Array)
83
+ attributes.collect { |attr| create(attr, &block) }
84
+ else
85
+ object = new(attributes)
86
+ yield(object) if block_given?
87
+ object.save
88
+ object
89
+ end
90
+ end
62
91
 
63
92
  private
64
93
 
65
94
  def find_initial(options)
66
- # TODO: Look into fixing this to actually work
67
- #options.update(:max_results => 1)
95
+ options.update(:limit => 1)
68
96
  find_every(options)
69
97
  end
70
98
 
@@ -142,7 +170,7 @@ module SugarCRM; class Base
142
170
  unless column =~ /_c$/ # attribute name ending with _c implies a custom attribute
143
171
  condition_attribute = "#{self._module.table_name}.#{column}"
144
172
  else
145
- condition_attribute = column # if setting a condition on a customer attribute, don't add model table name (or query breaks)
173
+ condition_attribute = column # if setting a condition on a custom attribute (i.e. created by user in Studio), don't add model table name (or query breaks)
146
174
  end
147
175
  conditions << "#{condition_attribute} #{operator} \'#{value}\'"
148
176
  }
@@ -298,12 +326,11 @@ module SugarCRM; class Base
298
326
  # are using Base.establish_connection, you should be fine. But if you are
299
327
  # using the Connection class by itself, you may need to prime the pump with
300
328
  # a call to Module.register_all
301
- def initialize(id=nil, attributes={})
302
- @id = id
303
- @attributes = self.class.attributes_from_module_fields.merge(attributes)
329
+ def initialize(attributes={})
304
330
  @modified_attributes = {}
331
+ merge_attributes(attributes.with_indifferent_access)
332
+ clear_association_cache
305
333
  @associations = self.class.associations_from_module_link_fields
306
- @errors = Set.new
307
334
  define_attribute_methods
308
335
  define_association_methods
309
336
  typecast_attributes
@@ -325,28 +352,54 @@ module SugarCRM; class Base
325
352
  # Saves the current object, checks that required fields are present.
326
353
  # returns true or false
327
354
  def save
328
- return false unless changed?
329
- return false unless valid?
330
- # If we get a Hash back, return true. Otherwise return false.
331
- (SugarCRM.connection.set_entry(self.class._module.name, serialize_modified_attributes).class == Hash)
355
+ return false if !changed?
356
+ return false if !valid?
357
+ begin
358
+ save!
359
+ rescue
360
+ return false
361
+ end
362
+ true
332
363
  end
333
364
 
334
365
  # Saves the current object, checks that required fields are present.
335
366
  # raises an exception if a save fails
336
367
  def save!
337
- raise InvalidRecord, errors.to_a.join(", ") unless valid?
338
- # If we get a Hash back, return true. Otherwise return false.
339
- (SugarCRM.connection.set_entry(self.class._module.name, serialize_modified_attributes).class == Hash)
368
+ save_modified_attributes
369
+ save_modified_associations
370
+ true
340
371
  end
341
372
 
342
373
  def delete
343
- return false if @id.blank?
374
+ return false if id.blank?
344
375
  params = {}
345
376
  params[:id] = serialize_id
346
377
  params[:deleted]= {:name => "deleted", :value => "1"}
347
378
  (SugarCRM.connection.set_entry(self.class._module.name, params).class == Hash)
348
379
  end
349
-
380
+
381
+ # Returns true if +comparison_object+ is the same exact object, or +comparison_object+
382
+ # is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
383
+ #
384
+ # Note that new records are different from any other record by definition, unless the
385
+ # other record is the receiver itself. Besides, if you fetch existing records with
386
+ # +select+ and leave the ID out, you're on your own, this predicate will return false.
387
+ #
388
+ # Note also that destroying a record preserves its ID in the model instance, so deleted
389
+ # models are still comparable.
390
+ def ==(comparison_object)
391
+ comparison_object.instance_of?(self.class) &&
392
+ id.present? &&
393
+ comparison_object.id == id
394
+ end
395
+
396
+
397
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
398
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
399
+ def hash
400
+ id.hash
401
+ end
402
+
350
403
 
351
404
  # Wrapper around class attribute
352
405
  def attribute_methods_generated?
@@ -360,6 +413,9 @@ module SugarCRM; class Base
360
413
  Base.class_eval do
361
414
  include AttributeMethods
362
415
  extend AttributeMethods::ClassMethods
416
+ include AttributeValidations
417
+ include AttributeTypeCast
418
+ include AttributeSerializers
363
419
  include AssociationMethods
364
420
  extend AssociationMethods::ClassMethods
365
421
  end
@@ -1,139 +1,5 @@
1
- require 'uri'
2
- require 'net/https'
3
-
4
- require 'rubygems'
5
- # TODO: Remove this dependency - ActiveSupport should cover it.
6
- require 'json'
7
-
8
1
  require 'sugarcrm/connection/helper'
2
+ require 'sugarcrm/connection/connection'
3
+ require 'sugarcrm/connection/request'
4
+ require 'sugarcrm/connection/response'
9
5
  Dir["#{File.dirname(__FILE__)}/connection/api/*.rb"].each { |f| load(f) }
10
-
11
- module SugarCRM; class Connection
12
-
13
- URL = "/service/v2/rest.php"
14
- DONT_SHOW_DEBUG_FOR = []
15
- RESPONSE_IS_NOT_JSON = [:get_user_id, :get_user_team_id]
16
-
17
- attr :url, true
18
- attr :user, false
19
- attr :pass, false
20
- attr :session, true
21
- attr :connection, true
22
- attr :options, true
23
- attr :request, true
24
- attr :response, true
25
-
26
- # This is the singleton connection class.
27
- def initialize(url, user, pass, options={})
28
- @options = {
29
- :debug => false,
30
- :register_modules => true
31
- }.merge(options)
32
-
33
- @url = URI.parse(url)
34
- @user = user
35
- @pass = pass
36
- @request = ""
37
- @response = ""
38
-
39
- resolve_url
40
- login!
41
- self
42
- end
43
-
44
- # Check to see if we are logged in
45
- def logged_in?
46
- @session ? true : false
47
- end
48
-
49
- # Login
50
- def login!
51
- @session = login["id"]
52
- raise SugarCRM::LoginError, "Invalid Login" unless logged_in?
53
- SugarCRM.connection = self
54
- SugarCRM::Base.connection = self
55
- Module.register_all if @options[:register_modules]
56
- end
57
-
58
- # Check to see if we are connected
59
- def connected?
60
- return false unless @connection
61
- return false unless @connection.started?
62
- true
63
- end
64
-
65
- # Connect
66
- def connect!
67
- @connection = Net::HTTP.new(@url.host, @url.port)
68
- if @url.scheme == "https"
69
- @connection.use_ssl = true
70
- @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
71
- end
72
- @connection.start
73
- end
74
-
75
- # Send a request to the Sugar Instance
76
- def send!(method, json)
77
- @request = SugarCRM::Request.new(@url, method, json, @options[:debug])
78
- if @request.length > 3900
79
- @response = @connection.post(@url.path, @request)
80
- else
81
- @response = @connection.get(@url.path.dup + "?" + @request.to_s)
82
- end
83
- handle_response
84
- end
85
-
86
- def debug=(debug)
87
- options[:debug] = debug
88
- end
89
-
90
- def debug?
91
- options[:debug]
92
- end
93
-
94
- private
95
-
96
- def handle_response
97
- case @response
98
- when Net::HTTPOK
99
- return process_response
100
- when Net::HTTPNotFound
101
- raise SugarCRM::InvalidSugarCRMUrl, "#{@url} is invalid"
102
- when Net::HTTPInternalServerError
103
- raise SugarCRM::InvalidRequest, "#{@request} is invalid"
104
- else
105
- if @options[:debug]
106
- puts "#{@request.method}: Raw Response:"
107
- puts @response.body
108
- puts "\n"
109
- end
110
- raise SugarCRM::UnhandledResponse, "Can't handle response #{@response}"
111
- end
112
- end
113
-
114
- def process_response
115
- # Complain if our body is empty.
116
- raise SugarCRM::EmptyResponse unless @response.body
117
- # Some methods are dumb and don't return a JSON Response
118
- return @response.body if RESPONSE_IS_NOT_JSON.include? @request.method
119
- # Push it through the old meat grinder.
120
- response_json = JSON.parse @response.body
121
- # Empty result. Is this wise?
122
- return false if response_json["result_count"] == 0
123
- # Filter debugging on REALLY BIG responses
124
- if @options[:debug] && !(DONT_SHOW_DEBUG_FOR.include? @request.method)
125
- puts "#{@request.method}: JSON Response:"
126
- pp response_json
127
- puts "\n"
128
- end
129
- return response_json
130
- end
131
-
132
- def resolve_url
133
- # Appends the rest.php path onto the end of the URL if it's not included
134
- if @url.path !~ /rest.php$/
135
- @url.path += URL
136
- end
137
- end
138
-
139
- end; end
@@ -2,7 +2,7 @@ module SugarCRM; class Connection
2
2
  # Retrieve a list of SugarBeans. This is the primary method for getting
3
3
  # a list of SugarBeans using the REST API.
4
4
  def get_entry_list(module_name, query, opts={})
5
- login! unless logged_in?
5
+ login! unless logged_in?
6
6
  options = {
7
7
  :order_by => '',
8
8
  :offset => '',