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.
Files changed (94) hide show
  1. data/.document +5 -0
  2. data/.gitignore +29 -0
  3. data/Gemfile +14 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +275 -0
  6. data/Rakefile +44 -0
  7. data/VERSION +1 -0
  8. data/WATCHLIST.rdoc +7 -0
  9. data/bin/sugarcrm +26 -0
  10. data/lib/rails/generators/sugarcrm/config/config_generator.rb +22 -0
  11. data/lib/rails/generators/sugarcrm/config/templates/initializer.rb +4 -0
  12. data/lib/rails/generators/sugarcrm/config/templates/sugarcrm.yml +19 -0
  13. data/lib/sugarcrm/associations/association.rb +170 -0
  14. data/lib/sugarcrm/associations/association_cache.rb +36 -0
  15. data/lib/sugarcrm/associations/association_collection.rb +141 -0
  16. data/lib/sugarcrm/associations/association_methods.rb +91 -0
  17. data/lib/sugarcrm/associations/associations.rb +61 -0
  18. data/lib/sugarcrm/associations.rb +5 -0
  19. data/lib/sugarcrm/attributes/attribute_methods.rb +203 -0
  20. data/lib/sugarcrm/attributes/attribute_serializers.rb +55 -0
  21. data/lib/sugarcrm/attributes/attribute_typecast.rb +44 -0
  22. data/lib/sugarcrm/attributes/attribute_validations.rb +62 -0
  23. data/lib/sugarcrm/attributes.rb +4 -0
  24. data/lib/sugarcrm/base.rb +355 -0
  25. data/lib/sugarcrm/config/sugarcrm.yaml +10 -0
  26. data/lib/sugarcrm/connection/api/get_available_modules.rb +22 -0
  27. data/lib/sugarcrm/connection/api/get_document_revision.rb +14 -0
  28. data/lib/sugarcrm/connection/api/get_entries.rb +23 -0
  29. data/lib/sugarcrm/connection/api/get_entries_count.rb +20 -0
  30. data/lib/sugarcrm/connection/api/get_entry.rb +23 -0
  31. data/lib/sugarcrm/connection/api/get_entry_list.rb +31 -0
  32. data/lib/sugarcrm/connection/api/get_module_fields.rb +15 -0
  33. data/lib/sugarcrm/connection/api/get_note_attachment.rb +14 -0
  34. data/lib/sugarcrm/connection/api/get_relationships.rb +30 -0
  35. data/lib/sugarcrm/connection/api/get_report_entries.rb +17 -0
  36. data/lib/sugarcrm/connection/api/get_server_info.rb +7 -0
  37. data/lib/sugarcrm/connection/api/get_user_id.rb +13 -0
  38. data/lib/sugarcrm/connection/api/get_user_team_id.rb +14 -0
  39. data/lib/sugarcrm/connection/api/login.rb +18 -0
  40. data/lib/sugarcrm/connection/api/logout.rb +15 -0
  41. data/lib/sugarcrm/connection/api/seamless_login.rb +13 -0
  42. data/lib/sugarcrm/connection/api/search_by_module.rb +25 -0
  43. data/lib/sugarcrm/connection/api/set_campaign_merge.rb +15 -0
  44. data/lib/sugarcrm/connection/api/set_document_revision.rb +35 -0
  45. data/lib/sugarcrm/connection/api/set_entries.rb +15 -0
  46. data/lib/sugarcrm/connection/api/set_entry.rb +15 -0
  47. data/lib/sugarcrm/connection/api/set_note_attachment.rb +25 -0
  48. data/lib/sugarcrm/connection/api/set_relationship.rb +27 -0
  49. data/lib/sugarcrm/connection/api/set_relationships.rb +22 -0
  50. data/lib/sugarcrm/connection/connection.rb +201 -0
  51. data/lib/sugarcrm/connection/helper.rb +50 -0
  52. data/lib/sugarcrm/connection/request.rb +61 -0
  53. data/lib/sugarcrm/connection/response.rb +91 -0
  54. data/lib/sugarcrm/connection.rb +5 -0
  55. data/lib/sugarcrm/connection_pool.rb +163 -0
  56. data/lib/sugarcrm/exceptions.rb +23 -0
  57. data/lib/sugarcrm/extensions/README.txt +23 -0
  58. data/lib/sugarcrm/finders/dynamic_finder_match.rb +41 -0
  59. data/lib/sugarcrm/finders/finder_methods.rb +243 -0
  60. data/lib/sugarcrm/finders.rb +2 -0
  61. data/lib/sugarcrm/module.rb +174 -0
  62. data/lib/sugarcrm/module_methods.rb +91 -0
  63. data/lib/sugarcrm/session.rb +218 -0
  64. data/lib/sugarcrm.rb +22 -0
  65. data/sugarcrm.gemspec +178 -0
  66. data/test/config_test.yaml +15 -0
  67. data/test/connection/test_get_available_modules.rb +9 -0
  68. data/test/connection/test_get_entries.rb +15 -0
  69. data/test/connection/test_get_entry.rb +22 -0
  70. data/test/connection/test_get_entry_list.rb +23 -0
  71. data/test/connection/test_get_module_fields.rb +11 -0
  72. data/test/connection/test_get_relationships.rb +12 -0
  73. data/test/connection/test_get_server_info.rb +9 -0
  74. data/test/connection/test_get_user_id.rb +9 -0
  75. data/test/connection/test_get_user_team_id.rb +9 -0
  76. data/test/connection/test_login.rb +9 -0
  77. data/test/connection/test_logout.rb +9 -0
  78. data/test/connection/test_set_document_revision.rb +28 -0
  79. data/test/connection/test_set_entry.rb +15 -0
  80. data/test/connection/test_set_note_attachment.rb +16 -0
  81. data/test/connection/test_set_relationship.rb +18 -0
  82. data/test/extensions_test/patch.rb +9 -0
  83. data/test/helper.rb +17 -0
  84. data/test/test_association_collection.rb +11 -0
  85. data/test/test_associations.rb +156 -0
  86. data/test/test_connection.rb +13 -0
  87. data/test/test_connection_pool.rb +40 -0
  88. data/test/test_finders.rb +201 -0
  89. data/test/test_module.rb +51 -0
  90. data/test/test_request.rb +35 -0
  91. data/test/test_response.rb +26 -0
  92. data/test/test_session.rb +136 -0
  93. data/test/test_sugarcrm.rb +213 -0
  94. 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,5 @@
1
+ require 'sugarcrm/associations/association'
2
+ require 'sugarcrm/associations/associations'
3
+ require 'sugarcrm/associations/association_cache'
4
+ require 'sugarcrm/associations/association_methods'
5
+ require 'sugarcrm/associations/association_collection'
@@ -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
@@ -0,0 +1,4 @@
1
+ require 'sugarcrm/attributes/attribute_methods'
2
+ require 'sugarcrm/attributes/attribute_validations'
3
+ require 'sugarcrm/attributes/attribute_serializers'
4
+ require 'sugarcrm/attributes/attribute_typecast'