activeforce 1.5.0

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