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.
- data/README.rdoc +58 -19
- data/Rakefile +1 -4
- data/VERSION +1 -1
- data/lib/sugarcrm.rb +7 -4
- data/lib/sugarcrm/associations.rb +2 -0
- data/lib/sugarcrm/associations/association_collection.rb +143 -0
- data/lib/sugarcrm/associations/association_methods.rb +92 -0
- data/lib/sugarcrm/attributes.rb +4 -0
- data/lib/sugarcrm/attributes/attribute_methods.rb +131 -0
- data/lib/sugarcrm/attributes/attribute_serializers.rb +55 -0
- data/lib/sugarcrm/attributes/attribute_typecast.rb +39 -0
- data/lib/sugarcrm/attributes/attribute_validations.rb +37 -0
- data/lib/sugarcrm/base.rb +77 -21
- data/lib/sugarcrm/connection.rb +3 -137
- data/lib/sugarcrm/connection/api/get_entry_list.rb +1 -1
- data/lib/sugarcrm/connection/api/get_note_attachment.rb +0 -1
- data/lib/sugarcrm/connection/api/set_relationship.rb +3 -0
- data/lib/sugarcrm/connection/connection.rb +144 -0
- data/lib/sugarcrm/{request.rb → connection/request.rb} +2 -10
- data/lib/sugarcrm/{response.rb → connection/response.rb} +10 -4
- data/lib/sugarcrm/exceptions.rb +14 -26
- data/lib/sugarcrm/module.rb +19 -18
- data/test/connection/test_get_entry.rb +5 -5
- data/test/connection/test_get_module_fields.rb +1 -1
- data/test/connection/test_set_relationship.rb +13 -21
- data/test/helper.rb +1 -1
- data/test/test_association_collection.rb +12 -0
- data/test/test_associations.rb +33 -0
- data/test/test_connection.rb +0 -7
- data/test/test_module.rb +1 -1
- data/test/test_sugarcrm.rb +16 -8
- metadata +22 -28
- data/lib/sugarcrm/association_methods.rb +0 -46
- 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
|
data/lib/sugarcrm/base.rb
CHANGED
@@ -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
|
-
|
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
|
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(
|
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
|
329
|
-
return false
|
330
|
-
|
331
|
-
|
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
|
-
|
338
|
-
|
339
|
-
|
368
|
+
save_modified_attributes
|
369
|
+
save_modified_associations
|
370
|
+
true
|
340
371
|
end
|
341
372
|
|
342
373
|
def delete
|
343
|
-
return false if
|
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
|
data/lib/sugarcrm/connection.rb
CHANGED
@@ -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 => '',
|