activeforce 1.5.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 (86) hide show
  1. data/Gemfile +15 -0
  2. data/Gemfile.lock +128 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.md +112 -0
  5. data/Rakefile +47 -0
  6. data/VERSION +1 -0
  7. data/activeforce.gemspec +151 -0
  8. data/app/models/salesforce/account.rb +4 -0
  9. data/app/models/salesforce/activity_history.rb +4 -0
  10. data/app/models/salesforce/approval.rb +4 -0
  11. data/app/models/salesforce/campaign.rb +4 -0
  12. data/app/models/salesforce/campaign_feed.rb +4 -0
  13. data/app/models/salesforce/campaign_member.rb +4 -0
  14. data/app/models/salesforce/case.rb +4 -0
  15. data/app/models/salesforce/case_comment.rb +4 -0
  16. data/app/models/salesforce/case_contact_role.rb +4 -0
  17. data/app/models/salesforce/case_feed.rb +4 -0
  18. data/app/models/salesforce/case_history.rb +4 -0
  19. data/app/models/salesforce/case_share.rb +4 -0
  20. data/app/models/salesforce/case_solution.rb +4 -0
  21. data/app/models/salesforce/case_status.rb +4 -0
  22. data/app/models/salesforce/case_team_member.rb +4 -0
  23. data/app/models/salesforce/community.rb +4 -0
  24. data/app/models/salesforce/contact.rb +4 -0
  25. data/app/models/salesforce/contact_feed.rb +4 -0
  26. data/app/models/salesforce/contact_history.rb +4 -0
  27. data/app/models/salesforce/contract.rb +4 -0
  28. data/app/models/salesforce/document.rb +4 -0
  29. data/app/models/salesforce/event.rb +4 -0
  30. data/app/models/salesforce/feed_item.rb +4 -0
  31. data/app/models/salesforce/group.rb +4 -0
  32. data/app/models/salesforce/group_member.rb +4 -0
  33. data/app/models/salesforce/idea.rb +4 -0
  34. data/app/models/salesforce/lead.rb +4 -0
  35. data/app/models/salesforce/lead_status.rb +4 -0
  36. data/app/models/salesforce/name.rb +4 -0
  37. data/app/models/salesforce/note.rb +4 -0
  38. data/app/models/salesforce/open_activity.rb +4 -0
  39. data/app/models/salesforce/opportunity.rb +4 -0
  40. data/app/models/salesforce/organization.rb +4 -0
  41. data/app/models/salesforce/partner.rb +4 -0
  42. data/app/models/salesforce/period.rb +4 -0
  43. data/app/models/salesforce/product2.rb +4 -0
  44. data/app/models/salesforce/product2_feed.rb +4 -0
  45. data/app/models/salesforce/profile.rb +4 -0
  46. data/app/models/salesforce/quote.rb +4 -0
  47. data/app/models/salesforce/solution.rb +4 -0
  48. data/app/models/salesforce/task.rb +4 -0
  49. data/app/models/salesforce/task_feed.rb +4 -0
  50. data/app/models/salesforce/task_priority.rb +4 -0
  51. data/app/models/salesforce/task_status.rb +4 -0
  52. data/app/models/salesforce/user.rb +4 -0
  53. data/app/models/salesforce/user_role.rb +4 -0
  54. data/app/models/salesforce/vote.rb +4 -0
  55. data/lib/activeforce.rb +29 -0
  56. data/lib/salesforce/attributes.rb +13 -0
  57. data/lib/salesforce/authentication.rb +25 -0
  58. data/lib/salesforce/base.rb +240 -0
  59. data/lib/salesforce/bulk/batch.rb +77 -0
  60. data/lib/salesforce/bulk/job.rb +103 -0
  61. data/lib/salesforce/bulk/operations.rb +25 -0
  62. data/lib/salesforce/bulk/update_job.rb +24 -0
  63. data/lib/salesforce/column.rb +98 -0
  64. data/lib/salesforce/columns.rb +60 -0
  65. data/lib/salesforce/config.rb +110 -0
  66. data/lib/salesforce/connection.rb +33 -0
  67. data/lib/salesforce/connection/async.rb +36 -0
  68. data/lib/salesforce/connection/conversion.rb +37 -0
  69. data/lib/salesforce/connection/http_methods.rb +72 -0
  70. data/lib/salesforce/connection/rest_api.rb +52 -0
  71. data/lib/salesforce/connection/soap_api.rb +74 -0
  72. data/lib/salesforce/engine.rb +6 -0
  73. data/lib/salesforce/errors.rb +33 -0
  74. data/test/salesforce/authentication_test.rb +52 -0
  75. data/test/salesforce/base_test.rb +440 -0
  76. data/test/salesforce/bulk/batch_test.rb +79 -0
  77. data/test/salesforce/bulk/update_job_test.rb +125 -0
  78. data/test/salesforce/column_test.rb +115 -0
  79. data/test/salesforce/config_test.rb +163 -0
  80. data/test/salesforce/connection/async_test.rb +138 -0
  81. data/test/salesforce/connection/http_methods_test.rb +242 -0
  82. data/test/salesforce/connection/rest_api_test.rb +61 -0
  83. data/test/salesforce/connection/soap_api_test.rb +148 -0
  84. data/test/salesforce/connection_test.rb +148 -0
  85. data/test/test_helper.rb +79 -0
  86. metadata +295 -0
@@ -0,0 +1,77 @@
1
+ module Salesforce
2
+ module Bulk
3
+ class Batch
4
+ Result = Struct.new :id, :success, :created, :error
5
+ include ::Salesforce::Attributes
6
+
7
+ attr_accessor :id, :job, :number_records_failed, :number_records_processed, :state, :state_message, :total_time_processed, :filename, :csv
8
+
9
+ include Blockenspiel::DSL
10
+
11
+ def initialize(job)
12
+ self.job = job
13
+ self.filename = temporary_csv_file
14
+ self.csv = FasterCSV.open(self.filename, 'w+')
15
+ self.csv << csv_header
16
+ end
17
+
18
+ def record(record)
19
+ if record.is_a?(Hash)
20
+ self.csv << ordered_values(record)
21
+ else
22
+ self.csv << ordered_values(record.attributes)
23
+ end
24
+ end
25
+
26
+ def ordered_values(record)
27
+ job.csv_columns.map do |col|
28
+ raw_value = record[col.name.to_sym]
29
+ Column.to_csv_value Column.typecast(col.type, raw_value)
30
+ end
31
+ end
32
+
33
+ def create!
34
+ self.csv.close
35
+ response = ::Salesforce.connection.async_post("job/#{job.id}/batch", File.read(self.filename), :format => :xml, :content_type => 'text/csv')
36
+ assign_attributes!(response)
37
+ end
38
+
39
+ def update_status
40
+ return state if completed?
41
+ response = ::Salesforce.connection.async_get("job/#{job.id}/batch/#{id}", :format => :xml)
42
+ self.state = response[:state]
43
+ end
44
+
45
+ def results
46
+ parse_csv_results ::Salesforce.connection.async_get("job/#{job.id}/batch/#{id}/result")
47
+ end
48
+
49
+ [ :queued, :in_progress, :completed, :failed, :not_processed ].each do |status|
50
+ define_method "#{status}?" do
51
+ self.state == status.to_s.titleize
52
+ end
53
+ end
54
+
55
+ def temporary_csv_file
56
+ if Object.const_defined?(:Rails)
57
+ Rails.root.join('tmp', 'files', "#{ Time.now.to_i}#{rand(10000)}.csv")
58
+ else
59
+ File.join("/tmp/#{ Time.now.to_i}#{rand(10000)}.csv")
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def parse_csv_results(results)
66
+ parsed_results = FasterCSV.parse(results)
67
+ parsed_results[1..-1].map { |row| Result.new(*row) }
68
+ end
69
+
70
+ def csv_header
71
+ self.job.csv_columns.map(&:original_name)
72
+ end
73
+
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,103 @@
1
+ module Salesforce
2
+ module Bulk
3
+ class Job
4
+ include ::Salesforce::Attributes
5
+
6
+ include Blockenspiel::DSL
7
+
8
+ attr_accessor :id, :operation, :object, :state, :concurrency_mode, :content_type, :number_of_batches_queued,
9
+ :number_batches_in_progress, :number_batches_completed, :number_batches_failed, :number_batches_total,
10
+ :number_records_processed, :number_retries, :object_type, :batches, :columns
11
+
12
+ [ :open, :closed, :aborted, :failed ].each do |status|
13
+ define_method "#{status}?" do
14
+ self.state == status.to_s.titleize
15
+ end
16
+ end
17
+
18
+ def initialize(object_type, operation, columns = :all)
19
+ self.object_type = object_type
20
+ self.object = object_type.table_name
21
+ self.operation = operation.to_s.downcase
22
+ self.parallel!
23
+ self.batches = []
24
+ self.columns = columns
25
+ end
26
+
27
+ def parallel!
28
+ self.concurrency_mode = "Parallel"
29
+ end
30
+
31
+ def serial!
32
+ self.concurrency_mode = "Serial"
33
+ end
34
+
35
+ def batch(&block)
36
+ Batch.new(self).tap do |batch|
37
+ Blockenspiel.invoke(block, batch)
38
+ self.batches << batch
39
+ end
40
+ end
41
+
42
+ def process!
43
+ create_job!
44
+ create_batches!
45
+ close_job!
46
+ end
47
+
48
+ def completed?
49
+ self.batches.each(&:update_status)
50
+ self.batches.all? { |batch| batch.completed? || batch.failed? }
51
+ end
52
+
53
+ def results
54
+ self.batches.map(&:results).flatten
55
+ end
56
+
57
+ private
58
+
59
+ def create_job!
60
+ response = ::Salesforce.connection.async_post("job", create_job_xml, :format => :xml)
61
+ assign_attributes!(response)
62
+ end
63
+
64
+ def create_batches!
65
+ batches.each(&:create!)
66
+ end
67
+
68
+ def close_job!
69
+ response = ::Salesforce.connection.async_post("job/#{id}", close_job_xml, :format => :xml)
70
+ assign_attributes!(response)
71
+ end
72
+
73
+ def create_job_xml
74
+ job_xml do |job_info|
75
+ job_info.operation self.operation
76
+ job_info.object self.object
77
+ job_info.contentType "CSV"
78
+ end
79
+ end
80
+
81
+ def close_job_xml
82
+ job_xml do |job_info|
83
+ job_info.state "Closed"
84
+ end
85
+ end
86
+
87
+ def job_xml(&block)
88
+ xml = Builder::XmlMarkup.new(:indent => 2)
89
+ xml.instruct!
90
+ xml.jobInfo :xmlns => "http://www.force.com/2009/06/asyncapi/dataload" do |job_info|
91
+ block.call(job_info)
92
+ end
93
+ xml.target!
94
+ end
95
+
96
+ def csv_header
97
+ csv_columns.map(&:original_name)
98
+ end
99
+
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,25 @@
1
+ module Salesforce
2
+ module Bulk
3
+ module Operations
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Create a bulk update job
8
+ # job = Salesforce::Account.bulk_update do
9
+ # batch do
10
+ # record account_1
11
+ # record account_2
12
+ # end
13
+ # end
14
+ #
15
+
16
+ def bulk_update(columns = [], &block)
17
+ UpdateJob.new(self, columns).tap do |job|
18
+ Blockenspiel.invoke(block, job)
19
+ job.process!
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ module Salesforce
2
+ module Bulk
3
+ class UpdateJob < Job
4
+
5
+ def initialize(object_type, columns = :all)
6
+ super(object_type, 'update', columns)
7
+ end
8
+
9
+ def csv_columns
10
+ [ object_type.columns.id_column ] + if columns.blank? || columns == :all
11
+ object_type.columns.updateable
12
+ else
13
+ columns.map do |col|
14
+ sf_col = object_type.columns.find { |scol| scol.name == col.to_s }
15
+ raise UnrecognizedColumn.new("#{col} is not a valid column.") unless sf_col
16
+ sf_col
17
+ end
18
+ end
19
+ end
20
+
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,98 @@
1
+ module Salesforce
2
+ class Column
3
+ attr_accessor :name, :original_name, :createable, :updateable, :type
4
+
5
+ def initialize(field)
6
+ self.original_name = field["name"]
7
+ self.name = field["name"].gsub(/\_\_c$/, '').underscore
8
+ self.type = field["type"].to_sym
9
+ self.createable = field['createable']
10
+ self.updateable = field["updateable"]
11
+ end
12
+
13
+ def createable?
14
+ createable
15
+ end
16
+
17
+ def updateable?
18
+ updateable
19
+ end
20
+
21
+ def editable?
22
+ createable? || updateable?
23
+ end
24
+
25
+
26
+ def self.to_soql_value(obj)
27
+ case (obj)
28
+ when Date
29
+ obj.strftime("%Y-%m-%d")
30
+ when TrueClass
31
+ 'TRUE'
32
+ when FalseClass
33
+ 'FALSE'
34
+ when Time
35
+ obj.xmlschema
36
+ when nil
37
+ 'NULL'
38
+ when Numeric
39
+ "#{obj.to_s}"
40
+ when Array
41
+ "(#{obj.map { |sobj| to_soql_value(sobj) }.join(',')})"
42
+ else
43
+ "'#{obj.to_s}'"
44
+ end
45
+ end
46
+
47
+ def self.to_csv_value(obj)
48
+ case (obj)
49
+ when Date;
50
+ obj.strftime("%Y-%m-%d")
51
+ when TrueClass;
52
+ 'TRUE'
53
+ when FalseClass;
54
+ 'FALSE'
55
+ when Time;
56
+ obj.xmlschema
57
+ else
58
+ "#{obj.to_s}"
59
+ end
60
+ end
61
+
62
+ def self.typecast(type, value)
63
+ case (type)
64
+ when :id, :reference
65
+ if Config.use_full_length_ids?
66
+ value
67
+ else
68
+ value.to_s.size == 15 ? value : value.to_s[0..14]
69
+ end
70
+ when :date
71
+ begin
72
+ Date.parse(value);
73
+ rescue
74
+ value if value.is_a?(Date)
75
+ end
76
+ when :datetime
77
+ begin
78
+ Time.parse(value)
79
+ rescue
80
+ value if value.is_a?(Time)
81
+ end
82
+ when :double
83
+ begin
84
+ BigDecimal(value.to_s)
85
+ rescue
86
+ value if value.is_a?(Numeric)
87
+ end
88
+ else
89
+ value
90
+ end
91
+ end
92
+
93
+ def ==(other)
94
+ return false unless other
95
+ self.name == other.name && self.original_name == other.original_name
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,60 @@
1
+ module Salesforce
2
+ class Columns
3
+ include Enumerable
4
+ attr_accessor :by_name, :by_original_name, :table_name
5
+
6
+ def initialize(table_name)
7
+ self.table_name = table_name
8
+ fields = Connection.fields(table_name)
9
+ self.by_name = {}
10
+ self.by_original_name = {}
11
+ fields.each do |field|
12
+ column = Column.new(field)
13
+ by_name[column.name] = column
14
+ by_original_name[column.original_name] = column
15
+ end
16
+ end
17
+
18
+ def each(&block)
19
+ all.each(&block)
20
+ end
21
+
22
+ def all
23
+ @all ||= by_name.values.flatten
24
+ end
25
+
26
+ def editable
27
+ @editable ||= select(&:editable?)
28
+ end
29
+
30
+ def createable
31
+ select(&:createable?)
32
+ end
33
+
34
+ def updateable
35
+ select(&:updateable?)
36
+ end
37
+
38
+ def id_column
39
+ find { |col| col.name.to_sym == :id }
40
+ end
41
+
42
+ def names
43
+ map(&:name)
44
+ end
45
+
46
+ def soql_selector
47
+ @soql_selector ||= by_original_name.keys.sort.join(',')
48
+ end
49
+
50
+ def ==(other)
51
+ other && self.all.map(&:name) == other.all.map(&:name)
52
+ end
53
+
54
+ def find_by_name(name)
55
+ column = by_name[name.to_s]
56
+ raise ColumnNotFound.new(name, table_name) unless column
57
+ column
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,110 @@
1
+ module Salesforce
2
+ class Config
3
+
4
+ DEFAULT_API_VERSION = "22.0"
5
+
6
+ include Blockenspiel::DSL
7
+ include Blockenspiel::DSLSetupMethods
8
+
9
+ dsl_attr_accessor :session_id, :server_instance, :user_id, :soap_endpoint_url
10
+
11
+ [
12
+ :username, :password, :api_version, :use_sandbox?, :use_full_length_ids?,
13
+ :login_url, :session_id, :server_instance, :soap_endpoint_url, :soap_enterprise_namespace,
14
+ :user_id, :server_url, :server_host, :async_url, :configured?, :on_login_failure ].each do |method_name|
15
+ eval <<-RUBY
16
+ def self.#{method_name}
17
+ instance.#{method_name}
18
+ end
19
+ RUBY
20
+ end
21
+
22
+ def self.instance
23
+ @instance ||= new
24
+ end
25
+
26
+ def username(*args, &block)
27
+ if block.present?
28
+ @username = Proc.new { block.call }
29
+ elsif args.present?
30
+ @username = args.first
31
+ elsif @username.respond_to?(:call)
32
+ @username.call
33
+ else
34
+ @username
35
+ end
36
+ end
37
+
38
+ def password(*args, &block)
39
+ if block.present?
40
+ @password = Proc.new { block.call }
41
+ elsif args.present?
42
+ @password = args.first
43
+ elsif @password.respond_to?(:call)
44
+ @password.call
45
+ else
46
+ @password
47
+ end
48
+ end
49
+
50
+ def api_version(val = nil)
51
+ if val
52
+ @api_version = val.to_f.to_s
53
+ else
54
+ @api_version || DEFAULT_API_VERSION
55
+ end
56
+ end
57
+
58
+ def use_sandbox?
59
+ @use_sandbox || false
60
+ end
61
+
62
+ def use_full_length_ids?
63
+ @use_full_length_ids || false
64
+ end
65
+
66
+ def use_full_length_ids
67
+ @use_full_length_ids = true
68
+ end
69
+
70
+ def use_sandbox
71
+ @use_sandbox = true
72
+ end
73
+
74
+ def on_login_failure(&block)
75
+ if block.present?
76
+ @on_login_failure = Proc.new { block.call }
77
+ else
78
+ @on_login_failure.try(:call)
79
+ end
80
+ end
81
+
82
+ def configured?
83
+ username.present? && password.present?
84
+ end
85
+
86
+ def soap_enterprise_namespace
87
+ 'urn:enterprise.soap.sforce.com'
88
+ end
89
+
90
+ def server_url
91
+ "https://#{server_instance}.salesforce.com/services/data/v#{api_version}"
92
+ end
93
+
94
+ def server_host
95
+ "https://#{server_instance}.salesforce.com"
96
+ end
97
+
98
+ def async_url
99
+ "https://#{server_instance}.salesforce.com/services/async/#{api_version}"
100
+ end
101
+
102
+ def login_url
103
+ login_url_base + api_version
104
+ end
105
+
106
+ def login_url_base
107
+ use_sandbox? ? 'https://test.salesforce.com/services/Soap/c/' : 'https://login.salesforce.com/services/Soap/c/'
108
+ end
109
+ end
110
+ end