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