sugarcrm_emp 0.10.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/.document +5 -0
- data/.gitignore +29 -0
- data/Gemfile +14 -0
- data/LICENSE +20 -0
- data/README.rdoc +275 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/WATCHLIST.rdoc +7 -0
- data/bin/sugarcrm +26 -0
- data/lib/rails/generators/sugarcrm/config/config_generator.rb +22 -0
- data/lib/rails/generators/sugarcrm/config/templates/initializer.rb +4 -0
- data/lib/rails/generators/sugarcrm/config/templates/sugarcrm.yml +19 -0
- data/lib/sugarcrm/associations/association.rb +170 -0
- data/lib/sugarcrm/associations/association_cache.rb +36 -0
- data/lib/sugarcrm/associations/association_collection.rb +141 -0
- data/lib/sugarcrm/associations/association_methods.rb +91 -0
- data/lib/sugarcrm/associations/associations.rb +61 -0
- data/lib/sugarcrm/associations.rb +5 -0
- data/lib/sugarcrm/attributes/attribute_methods.rb +203 -0
- data/lib/sugarcrm/attributes/attribute_serializers.rb +55 -0
- data/lib/sugarcrm/attributes/attribute_typecast.rb +44 -0
- data/lib/sugarcrm/attributes/attribute_validations.rb +62 -0
- data/lib/sugarcrm/attributes.rb +4 -0
- data/lib/sugarcrm/base.rb +355 -0
- data/lib/sugarcrm/config/sugarcrm.yaml +10 -0
- data/lib/sugarcrm/connection/api/get_available_modules.rb +22 -0
- data/lib/sugarcrm/connection/api/get_document_revision.rb +14 -0
- data/lib/sugarcrm/connection/api/get_entries.rb +23 -0
- data/lib/sugarcrm/connection/api/get_entries_count.rb +20 -0
- data/lib/sugarcrm/connection/api/get_entry.rb +23 -0
- data/lib/sugarcrm/connection/api/get_entry_list.rb +31 -0
- data/lib/sugarcrm/connection/api/get_module_fields.rb +15 -0
- data/lib/sugarcrm/connection/api/get_note_attachment.rb +14 -0
- data/lib/sugarcrm/connection/api/get_relationships.rb +30 -0
- data/lib/sugarcrm/connection/api/get_report_entries.rb +17 -0
- data/lib/sugarcrm/connection/api/get_server_info.rb +7 -0
- data/lib/sugarcrm/connection/api/get_user_id.rb +13 -0
- data/lib/sugarcrm/connection/api/get_user_team_id.rb +14 -0
- data/lib/sugarcrm/connection/api/login.rb +18 -0
- data/lib/sugarcrm/connection/api/logout.rb +15 -0
- data/lib/sugarcrm/connection/api/seamless_login.rb +13 -0
- data/lib/sugarcrm/connection/api/search_by_module.rb +25 -0
- data/lib/sugarcrm/connection/api/set_campaign_merge.rb +15 -0
- data/lib/sugarcrm/connection/api/set_document_revision.rb +35 -0
- data/lib/sugarcrm/connection/api/set_entries.rb +15 -0
- data/lib/sugarcrm/connection/api/set_entry.rb +15 -0
- data/lib/sugarcrm/connection/api/set_note_attachment.rb +25 -0
- data/lib/sugarcrm/connection/api/set_relationship.rb +27 -0
- data/lib/sugarcrm/connection/api/set_relationships.rb +22 -0
- data/lib/sugarcrm/connection/connection.rb +201 -0
- data/lib/sugarcrm/connection/helper.rb +50 -0
- data/lib/sugarcrm/connection/request.rb +61 -0
- data/lib/sugarcrm/connection/response.rb +91 -0
- data/lib/sugarcrm/connection.rb +5 -0
- data/lib/sugarcrm/connection_pool.rb +163 -0
- data/lib/sugarcrm/exceptions.rb +23 -0
- data/lib/sugarcrm/extensions/README.txt +23 -0
- data/lib/sugarcrm/finders/dynamic_finder_match.rb +41 -0
- data/lib/sugarcrm/finders/finder_methods.rb +243 -0
- data/lib/sugarcrm/finders.rb +2 -0
- data/lib/sugarcrm/module.rb +174 -0
- data/lib/sugarcrm/module_methods.rb +91 -0
- data/lib/sugarcrm/session.rb +218 -0
- data/lib/sugarcrm.rb +22 -0
- data/sugarcrm.gemspec +178 -0
- data/test/config_test.yaml +15 -0
- data/test/connection/test_get_available_modules.rb +9 -0
- data/test/connection/test_get_entries.rb +15 -0
- data/test/connection/test_get_entry.rb +22 -0
- data/test/connection/test_get_entry_list.rb +23 -0
- data/test/connection/test_get_module_fields.rb +11 -0
- data/test/connection/test_get_relationships.rb +12 -0
- data/test/connection/test_get_server_info.rb +9 -0
- data/test/connection/test_get_user_id.rb +9 -0
- data/test/connection/test_get_user_team_id.rb +9 -0
- data/test/connection/test_login.rb +9 -0
- data/test/connection/test_logout.rb +9 -0
- data/test/connection/test_set_document_revision.rb +28 -0
- data/test/connection/test_set_entry.rb +15 -0
- data/test/connection/test_set_note_attachment.rb +16 -0
- data/test/connection/test_set_relationship.rb +18 -0
- data/test/extensions_test/patch.rb +9 -0
- data/test/helper.rb +17 -0
- data/test/test_association_collection.rb +11 -0
- data/test/test_associations.rb +156 -0
- data/test/test_connection.rb +13 -0
- data/test/test_connection_pool.rb +40 -0
- data/test/test_finders.rb +201 -0
- data/test/test_module.rb +51 -0
- data/test/test_request.rb +35 -0
- data/test/test_response.rb +26 -0
- data/test/test_session.rb +136 -0
- data/test/test_sugarcrm.rb +213 -0
- metadata +266 -0
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
|
|
6
|
+
attr_reader :collection
|
|
7
|
+
|
|
8
|
+
# creates a new instance of an AssociationCollection
|
|
9
|
+
# Owner is the parent object, and association is the target
|
|
10
|
+
def initialize(owner, association, preload=false)
|
|
11
|
+
@loaded = false
|
|
12
|
+
@owner = owner
|
|
13
|
+
@association= association
|
|
14
|
+
load if preload
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def changed?
|
|
19
|
+
return false unless loaded?
|
|
20
|
+
return true if added.length > 0
|
|
21
|
+
return true if removed.length > 0
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def loaded?
|
|
26
|
+
@loaded
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def load
|
|
30
|
+
load_associated_records unless loaded?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reload
|
|
34
|
+
load_associated_records
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# return any added elements
|
|
38
|
+
def added
|
|
39
|
+
load
|
|
40
|
+
@collection - @original
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# return any removed elements
|
|
44
|
+
def removed
|
|
45
|
+
load
|
|
46
|
+
@original - @collection
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Removes a record from the collection, uses the id of the record as a test for inclusion.
|
|
50
|
+
def delete(record)
|
|
51
|
+
load
|
|
52
|
+
raise InvalidRecord, "#{record.class} does not have a valid :id!" if record.id.empty?
|
|
53
|
+
@collection.delete record
|
|
54
|
+
end
|
|
55
|
+
alias :remove :delete
|
|
56
|
+
|
|
57
|
+
# Checks if a record is included in the current collection. Uses id's as comparison
|
|
58
|
+
def include?(record)
|
|
59
|
+
load
|
|
60
|
+
@collection.include? record
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Add +records+ to this association, saving any unsaved records before adding them.
|
|
64
|
+
# Returns +self+ so method calls may be chained.
|
|
65
|
+
# Be sure to call save on the association to commit any association changes
|
|
66
|
+
def <<(record)
|
|
67
|
+
load
|
|
68
|
+
record.save! if record.new?
|
|
69
|
+
result = true
|
|
70
|
+
result = false if include?(record)
|
|
71
|
+
@owner.update_association_cache_for(@association, record, :add)
|
|
72
|
+
record.update_association_cache_for(record.associations.find!(@owner).link_field, @owner, :add)
|
|
73
|
+
result && self
|
|
74
|
+
end
|
|
75
|
+
alias :add :<<
|
|
76
|
+
|
|
77
|
+
# delegate undefined methods to the @collection array
|
|
78
|
+
# E.g. contact.cases should behave like an array and allow `length`, `size`, `each`, etc.
|
|
79
|
+
def method_missing(method_name, *args, &block)
|
|
80
|
+
load
|
|
81
|
+
@collection.send(method_name.to_sym, *args, &block)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# respond correctly for delegated methods
|
|
85
|
+
def respond_to?(method_name)
|
|
86
|
+
load
|
|
87
|
+
return true if @collection.respond_to? method_name
|
|
88
|
+
super
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def save
|
|
92
|
+
begin
|
|
93
|
+
save!
|
|
94
|
+
rescue
|
|
95
|
+
return false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Pushes collection changes to SugarCRM, and updates the state of the collection
|
|
100
|
+
def save!
|
|
101
|
+
load
|
|
102
|
+
added.each do |record|
|
|
103
|
+
associate!(record)
|
|
104
|
+
end
|
|
105
|
+
removed.each do |record|
|
|
106
|
+
disassociate!(record)
|
|
107
|
+
end
|
|
108
|
+
reload
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
protected
|
|
113
|
+
|
|
114
|
+
# Loads related records for the given association
|
|
115
|
+
def load_associated_records
|
|
116
|
+
array = @owner.class.session.connection.get_relationships(@owner.class._module.name, @owner.id, @association.to_s)
|
|
117
|
+
@loaded = true
|
|
118
|
+
# we use original to track the state of the collection at start
|
|
119
|
+
@collection = Array.wrap(array).dup
|
|
120
|
+
@original = Array.wrap(array).freeze
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Creates a relationship between the current object and the target
|
|
124
|
+
# Owner is the record the Collection is accessed from
|
|
125
|
+
# Target is the record we are adding to the collection
|
|
126
|
+
# i.e. user.email_addresses.associate!(EmailAddress.new(:email_address => "abc@abc.com"))
|
|
127
|
+
# user would be the owner, and EmailAddress.new() is the target
|
|
128
|
+
def associate!(target, opts={})
|
|
129
|
+
#target.save! if target.new?
|
|
130
|
+
@owner.associate!(target, opts)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Removes a relationship between the current object and the target
|
|
134
|
+
def disassociate!(target)
|
|
135
|
+
@owner.associate!(target,{:delete => 1})
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
alias :relate! :associate!
|
|
139
|
+
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module SugarCRM; module AssociationMethods
|
|
2
|
+
|
|
3
|
+
module ClassMethods
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
# Saves all modified associations.
|
|
7
|
+
def save_modified_associations!
|
|
8
|
+
@association_cache.values.each do |collection|
|
|
9
|
+
if collection.changed?
|
|
10
|
+
collection.save!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
true
|
|
14
|
+
end
|
|
15
|
+
|
|
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
|
|
24
|
+
# In contrast to using account.contacts << contact, this method doesn't load the relationships
|
|
25
|
+
# before setting the new relationship.
|
|
26
|
+
# This method is useful when certain modules have many links to other modules: not loading the
|
|
27
|
+
# relationships allows one to avoid a Timeout::Error
|
|
28
|
+
def associate!(target,opts={})
|
|
29
|
+
targets = Array.wrap(target)
|
|
30
|
+
targets.each do |t|
|
|
31
|
+
association = @associations.find!(t)
|
|
32
|
+
response = self.class.session.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}!"
|
|
39
|
+
end
|
|
40
|
+
# We need to update the association cache for any changes we make.
|
|
41
|
+
if opts[:delete] == 1
|
|
42
|
+
update_association_cache_for(association.link_field, t, :delete)
|
|
43
|
+
t.update_association_cache_for(association.link_field, self, :delete)
|
|
44
|
+
else
|
|
45
|
+
update_association_cache_for(association.link_field, t, :add)
|
|
46
|
+
t.update_association_cache_for(t.associations.find!(self).link_field, self, :add)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
alias :relate! :associate!
|
|
52
|
+
|
|
53
|
+
# Removes a relationship between the current object and the target object
|
|
54
|
+
def disassociate!(target)
|
|
55
|
+
associate!(target,{:delete => 1})
|
|
56
|
+
end
|
|
57
|
+
alias :unrelate! :disassociate!
|
|
58
|
+
|
|
59
|
+
protected
|
|
60
|
+
|
|
61
|
+
# Generates the association proxy methods for related modules
|
|
62
|
+
def define_association_methods
|
|
63
|
+
@associations = Associations.register(self)
|
|
64
|
+
return if association_methods_generated?
|
|
65
|
+
self.class.association_methods_generated = true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the records from the associated module or returns the cached copy if we've already
|
|
69
|
+
# loaded it. Force a reload of the records with reload=true
|
|
70
|
+
#
|
|
71
|
+
# {"email_addresses"=>
|
|
72
|
+
# {"name"=>"email_addresses",
|
|
73
|
+
# "module"=>"EmailAddress",
|
|
74
|
+
# "bean_name"=>"EmailAddress",
|
|
75
|
+
# "relationship"=>"users_email_addresses",
|
|
76
|
+
# "type"=>"link"},
|
|
77
|
+
#
|
|
78
|
+
def query_association(assoc, reload=false)
|
|
79
|
+
association = assoc.to_sym
|
|
80
|
+
return @association_cache[association] if association_cached?(association) && !reload
|
|
81
|
+
# TODO: Some relationships aren't fetchable via get_relationship (i.e users.contacts)
|
|
82
|
+
# even though get_module_fields lists them on the related_fields array. This is most
|
|
83
|
+
# commonly seen with one-to-many relationships without a join table. We need to cook
|
|
84
|
+
# up some elegant way to handle this.
|
|
85
|
+
collection = AssociationCollection.new(self,association,true)
|
|
86
|
+
# add it to the cache
|
|
87
|
+
@association_cache[association] = collection
|
|
88
|
+
collection
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end; end
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
# Returns the proxy methods of all the associations in the collection
|
|
23
|
+
def proxy_methods
|
|
24
|
+
@associations.inject([]) { |pm,a|
|
|
25
|
+
pm = pm | a.proxy_methods
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Looks up an association by object, link_field, or method.
|
|
30
|
+
# Raises an exception if not found
|
|
31
|
+
def find!(target)
|
|
32
|
+
@associations.each do |a|
|
|
33
|
+
return a if a.include? target
|
|
34
|
+
end
|
|
35
|
+
raise InvalidAssociation, "Could not lookup association for: #{target}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Looks up an association by object, link_field, or method.
|
|
39
|
+
# Returns false if not found
|
|
40
|
+
def find(association)
|
|
41
|
+
begin
|
|
42
|
+
find!(association)
|
|
43
|
+
rescue InvalidAssociation
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
alias :include? :find
|
|
48
|
+
|
|
49
|
+
# delegate undefined methods to the @collection array
|
|
50
|
+
# E.g. contact.cases should behave like an array and allow `length`, `size`, `each`, etc.
|
|
51
|
+
def method_missing(method_name, *args, &block)
|
|
52
|
+
@associations.send(method_name.to_sym, *args, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# respond correctly for delegated methods
|
|
56
|
+
def respond_to?(method_name)
|
|
57
|
+
return true if @associations.respond_to? method_name
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
module SugarCRM; module AttributeMethods
|
|
2
|
+
|
|
3
|
+
module ClassMethods
|
|
4
|
+
# Returns a hash of the module fields from the module
|
|
5
|
+
def attributes_from_module
|
|
6
|
+
fields = {}.with_indifferent_access
|
|
7
|
+
self._module.fields.keys.sort.each do |k|
|
|
8
|
+
fields[k] = nil
|
|
9
|
+
end
|
|
10
|
+
fields
|
|
11
|
+
end
|
|
12
|
+
# Returns the table name for a given attribute
|
|
13
|
+
def table_name_for(attribute)
|
|
14
|
+
table_name = self._module.table_name
|
|
15
|
+
if attribute.to_s =~ /_c$/
|
|
16
|
+
table_name = self._module.custom_table_name
|
|
17
|
+
end
|
|
18
|
+
table_name
|
|
19
|
+
end
|
|
20
|
+
# Takes a condition like: [:zip, ["> 75000", "< 80000"]]
|
|
21
|
+
# and flattens it to: ["accounts.zip > 75000", "accounts.zip < 80000"]
|
|
22
|
+
def flatten_conditions_for(condition)
|
|
23
|
+
conditions = []
|
|
24
|
+
attribute, attribute_conditions = condition
|
|
25
|
+
# Make sure we wrap the attribute condition in an array for EZ handling...
|
|
26
|
+
Array.wrap(attribute_conditions).each do |attribute_condition|
|
|
27
|
+
# parse operator in cases where:
|
|
28
|
+
# :attribute => '>= some_value',
|
|
29
|
+
# :attribute => "LIKE '%value%'",
|
|
30
|
+
# fallback to '=' operator as default]
|
|
31
|
+
operator = attribute_condition.to_s[/^([!<>=]*(LIKE|IS|NOT|\s)*)(.*)$/,1].strip!
|
|
32
|
+
# Default to = if we can't resolve the condition.
|
|
33
|
+
operator ||= '='
|
|
34
|
+
# Extract value from query
|
|
35
|
+
value = $3
|
|
36
|
+
unless attribute_condition.class == FalseClass
|
|
37
|
+
if attribute_condition.class == TrueClass
|
|
38
|
+
# fix value for checkboxes: users can pass true as condition, should be converted to '1' (false case for checkboxes is treated separately below)
|
|
39
|
+
value = (attribute_condition.class == TrueClass ? '1' : '0')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# TODO: Write a test for sending invalid attribute names.
|
|
43
|
+
# strip single quotes
|
|
44
|
+
value = value.strip[/'?([^']*)'?/,1]
|
|
45
|
+
conditions << "#{table_name_for(attribute)}.#{attribute} #{operator} \'#{value}\'"
|
|
46
|
+
else
|
|
47
|
+
# When a user creates a custom checkbox field, a column is added to the *_cstm table for that module (e.g. contacts_cstm for Contacts module).
|
|
48
|
+
# Each time a new record is created, the value of the checkbox will be stored in that _cstm table.
|
|
49
|
+
# However, records that exsited before that field was created are absent from the _cstm table.
|
|
50
|
+
# To return the expected results when a user is searching for records with an unchecked checkbox, we must return all records that aren't present in
|
|
51
|
+
# the _cstm table with a value of 1 (returning the record with 0 in the table will ignore the pre-existing records).
|
|
52
|
+
# Here, we create the appropriate query that will return all records that don't have a value of "true" in the checkbox field.
|
|
53
|
+
conditions << "#{self._module.table_name}.id NOT IN (SELECT id_c FROM #{table_name_for(attribute)} WHERE #{table_name_for(attribute)}.#{attribute} #{operator} 1)"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
conditions
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Determines if attributes or associations have been changed
|
|
61
|
+
def changed?
|
|
62
|
+
return true if attributes_changed?
|
|
63
|
+
return true if associations_changed?
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def destroyed?
|
|
68
|
+
@attributes[:deleted]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def attributes_changed?
|
|
72
|
+
@modified_attributes.length > 0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Is this a new record?
|
|
76
|
+
def new?
|
|
77
|
+
@attributes[:id].blank?
|
|
78
|
+
end
|
|
79
|
+
alias :new_record? :new?
|
|
80
|
+
|
|
81
|
+
# List the required attributes for save
|
|
82
|
+
def required_attributes
|
|
83
|
+
self.class._module.required_fields
|
|
84
|
+
end
|
|
85
|
+
alias :required_fields :required_attributes
|
|
86
|
+
|
|
87
|
+
def to_model
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
protected
|
|
92
|
+
|
|
93
|
+
# Merges attributes provided as an argument to initialize
|
|
94
|
+
# with attributes from the module.fields array. Skips any
|
|
95
|
+
# fields that aren't in the module.fields array
|
|
96
|
+
#
|
|
97
|
+
# BUG: SugarCRM likes to return fields you don't ask for and
|
|
98
|
+
# aren't fields on a module (i.e. modified_user_name). This
|
|
99
|
+
# royally screws up our typecasting code, so we handle it here.
|
|
100
|
+
def merge_attributes(attrs={})
|
|
101
|
+
# copy attributes from the parent module fields array
|
|
102
|
+
@attributes = self.class.attributes_from_module
|
|
103
|
+
# populate the attributes with values from the attrs provided to init.
|
|
104
|
+
@attributes.keys.each do |name|
|
|
105
|
+
write_attribute name, attrs[name] if attrs[name]
|
|
106
|
+
end
|
|
107
|
+
# If this is an existing record, blank out the modified_attributes hash
|
|
108
|
+
@modified_attributes = {} unless new?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Generates get/set methods for keys in the attributes hash
|
|
112
|
+
def define_attribute_methods
|
|
113
|
+
return if attribute_methods_generated?
|
|
114
|
+
@attributes.keys.sort.each do |k|
|
|
115
|
+
self.class.module_eval %Q?
|
|
116
|
+
def #{k}
|
|
117
|
+
read_attribute :#{k}
|
|
118
|
+
end
|
|
119
|
+
def #{k}=(value)
|
|
120
|
+
write_attribute :#{k},value
|
|
121
|
+
end
|
|
122
|
+
def #{k}\?
|
|
123
|
+
has_attribute\? :#{k}
|
|
124
|
+
end
|
|
125
|
+
?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# return the (polymorphic) parent record corresponding to the parent_id and parent_type attributes
|
|
129
|
+
# (an example of parent polymorphism can be found in the Note module)
|
|
130
|
+
if (@attributes.keys.include? 'parent_id') && (@attributes.keys.include? 'parent_type')
|
|
131
|
+
self.class.module_eval %Q?
|
|
132
|
+
def parent
|
|
133
|
+
(self.class.session.namespace_const.const_get @attributes['parent_type'].singularize).find(@attributes['parent_id'])
|
|
134
|
+
end
|
|
135
|
+
?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
self.class.attribute_methods_generated = true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns an <tt>#inspect</tt>-like string for the value of the
|
|
142
|
+
# attribute +attr_name+. String attributes are elided after 50
|
|
143
|
+
# characters, and Date and Time attributes are returned in the
|
|
144
|
+
# <tt>:db</tt> format. Other attributes return the value of
|
|
145
|
+
# <tt>#inspect</tt> without modification.
|
|
146
|
+
#
|
|
147
|
+
# person = Person.create!(:name => "David Heinemeier Hansson " * 3)
|
|
148
|
+
#
|
|
149
|
+
# person.attribute_for_inspect(:name)
|
|
150
|
+
# # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
|
|
151
|
+
#
|
|
152
|
+
# person.attribute_for_inspect(:created_at)
|
|
153
|
+
# # => '"2009-01-12 04:48:57"'
|
|
154
|
+
def attribute_for_inspect(attr_name)
|
|
155
|
+
value = read_attribute(attr_name)
|
|
156
|
+
if value.is_a?(String) && value.length > 50
|
|
157
|
+
"#{value[0..50]}...".inspect
|
|
158
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
|
159
|
+
%("#{value.to_s(:db)}")
|
|
160
|
+
else
|
|
161
|
+
value.inspect
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Wrapper for invoking save on modified_attributes
|
|
166
|
+
# sets the id if it's a new record
|
|
167
|
+
def save_modified_attributes!(opts={})
|
|
168
|
+
options = { :validate => true }.merge(opts)
|
|
169
|
+
if options[:validate]
|
|
170
|
+
# Complain if we aren't valid
|
|
171
|
+
raise InvalidRecord, @errors.full_messages.join(", ") unless valid?
|
|
172
|
+
end
|
|
173
|
+
# Send the save request
|
|
174
|
+
response = self.class.session.connection.set_entry(self.class._module.name, serialize_modified_attributes)
|
|
175
|
+
# Complain if we don't get a parseable response back
|
|
176
|
+
raise RecordsaveFailed, "Failed to save record: #{self}. Response was not a Hash" unless response.is_a? Hash
|
|
177
|
+
# Complain if we don't get a valid id back
|
|
178
|
+
raise RecordSaveFailed, "Failed to save record: #{self}. Response did not contain a valid 'id'." if response["id"].nil?
|
|
179
|
+
# Save the id to the record, if it's a new record
|
|
180
|
+
@attributes[:id] = response["id"] if new?
|
|
181
|
+
raise InvalidRecord, "Failed to update id for: #{self}." if id.nil?
|
|
182
|
+
# Clear the modified attributes Hash
|
|
183
|
+
@modified_attributes = {}
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Wrapper around attributes hash
|
|
188
|
+
def read_attribute(key)
|
|
189
|
+
@attributes[key]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Wrapper around attributes hash
|
|
193
|
+
def write_attribute(key, value)
|
|
194
|
+
@modified_attributes[key] = { :old => @attributes[key].to_s, :new => value }
|
|
195
|
+
@attributes[key] = value
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def has_attribute?(key)
|
|
199
|
+
@attributes.has_key? key
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
end; end
|
|
203
|
+
|
|
@@ -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", 5234 becomes "5234", etc.
|
|
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,44 @@
|
|
|
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
|
+
fields = self.class._module.fields
|
|
8
|
+
field = fields[attribute]
|
|
9
|
+
raise UninitializedModule, "#{self.class.session.namespace_const}Module #{self.class._module.name} was not initialized properly (fields.length == 0)" if fields.length == 0
|
|
10
|
+
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?
|
|
11
|
+
raise InvalidAttributeType, "#{self.class}._module.fields[#{attribute}] does not have a key for \'type\'" if field["type"].nil?
|
|
12
|
+
field["type"].to_sym
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Attempts to typecast each attribute based on the module field type
|
|
16
|
+
def typecast_attributes
|
|
17
|
+
@attributes.each_pair do |name,value|
|
|
18
|
+
# skip primary key columns
|
|
19
|
+
next if name == "id"
|
|
20
|
+
attr_type = attr_type_for(name)
|
|
21
|
+
|
|
22
|
+
# empty attributes should stay empty (e.g. an empty int field shouldn't be typecast as 0)
|
|
23
|
+
if [:datetime, :datetimecombo, :int].include? attr_type && (value.nil? || value == '')
|
|
24
|
+
@attributes[name] = nil
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
case attr_type
|
|
29
|
+
when :bool
|
|
30
|
+
@attributes[name] = (value == "1")
|
|
31
|
+
when :datetime, :datetimecombo
|
|
32
|
+
begin
|
|
33
|
+
@attributes[name] = DateTime.parse(value)
|
|
34
|
+
rescue
|
|
35
|
+
@attributes[name] = value
|
|
36
|
+
end
|
|
37
|
+
when :int
|
|
38
|
+
@attributes[name] = value.to_i
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
@attributes
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end; end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SugarCRM; module AttributeValidations
|
|
2
|
+
# Checks to see if we have all the neccessary attributes
|
|
3
|
+
def valid?
|
|
4
|
+
@errors = (defined?(HashWithIndifferentAccess) ? HashWithIndifferentAccess : ActiveSupport::HashWithIndifferentAccess).new
|
|
5
|
+
|
|
6
|
+
self.class._module.required_fields.each do |attribute|
|
|
7
|
+
valid_attribute?(attribute)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# for rails compatibility
|
|
11
|
+
def @errors.full_messages
|
|
12
|
+
# After removing attributes without errors, flatten the error hash, repeating the name of the attribute before each message:
|
|
13
|
+
# e.g. {'name' => ['cannot be blank', 'is too long'], 'website' => ['is not valid']}
|
|
14
|
+
# will become 'name cannot be blank, name is too long, website is not valid
|
|
15
|
+
self.inject([]){|memo, obj| memo.concat(obj[1].inject([]){|m, o| m << "#{obj[0].to_s.humanize} #{o}" })}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Rails needs each attribute to be present in the error hash (if the attribute has no error, it has [] as a value)
|
|
19
|
+
# Redefine the [] method for the errors hash to return [] instead of nil is the hash doesn't contain the key
|
|
20
|
+
class << @errors
|
|
21
|
+
alias :old_key_lookup :[]
|
|
22
|
+
def [](key)
|
|
23
|
+
old_key_lookup(key) || Array.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@errors.size == 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
protected
|
|
31
|
+
|
|
32
|
+
# TODO: Add test cases for validations
|
|
33
|
+
def valid_attribute?(attribute)
|
|
34
|
+
case attr_type_for(attribute)
|
|
35
|
+
when :bool
|
|
36
|
+
validate_class_for(attribute, [TrueClass, FalseClass])
|
|
37
|
+
when :datetime, :datetimecombo
|
|
38
|
+
validate_class_for(attribute, [DateTime])
|
|
39
|
+
when :int
|
|
40
|
+
validate_class_for(attribute, [Fixnum, Float])
|
|
41
|
+
else
|
|
42
|
+
if @attributes[attribute].blank?
|
|
43
|
+
add_error(attribute, "cannot be blank")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Compares the class of the attribute with the class or classes provided in the class array
|
|
49
|
+
# returns true if they match, otherwise adds an entry to the @errors collection, and returns false
|
|
50
|
+
def validate_class_for(attribute, class_array)
|
|
51
|
+
return true if class_array.include? @attributes[attribute].class
|
|
52
|
+
add_error(attribute, "must be a #{class_array.join(" or ")} object (not #{@attributes[attribute].class})")
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Add an error to the hash
|
|
57
|
+
def add_error(attribute, message)
|
|
58
|
+
@errors[attribute] ||= []
|
|
59
|
+
@errors[attribute] = @errors[attribute] << message unless @errors[attribute].include? message
|
|
60
|
+
@errors
|
|
61
|
+
end
|
|
62
|
+
end; end
|