adapi 0.0.9 → 0.1.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.
@@ -10,17 +10,28 @@ $keywords = Adapi::Keyword.new(
10
10
  :keywords => [ 'dem codez', '"top coder"', '[-code]' ]
11
11
  )
12
12
 
13
- $r = $keywords.create
13
+ $keywords.create
14
14
 
15
- # get array of keywords from Keyword instance
16
- $google_keywords = Adapi::Keyword.find(:all, :ad_group_id => $ad_group[:id]).keywords
15
+ if $keywords.errors.any?
17
16
 
18
- $params_keywords = Adapi::Keyword.parameterized($google_keywords)
17
+ puts "\nERRORS:"
18
+ pp $keywords.errors.messages
19
19
 
20
- $short_keywords = Adapi::Keyword.shortened($google_keywords)
20
+ else
21
21
 
22
- p "PARAMS:"
23
- pp $params_keywords
22
+ puts "\nKEYWORDS CREATED\n"
24
23
 
25
- p "\nSHORT:"
26
- pp $short_keywords
24
+ # get array of keywords from Keyword instance
25
+ $google_keywords = Adapi::Keyword.find(:all, :ad_group_id => $ad_group[:id]).keywords
26
+
27
+ $params_keywords = Adapi::Keyword.parameterized($google_keywords)
28
+
29
+ $short_keywords = Adapi::Keyword.shortened($google_keywords)
30
+
31
+ puts "DETAILED:"
32
+ pp $params_keywords
33
+
34
+ puts "\nSHORT:"
35
+ pp $short_keywords
36
+
37
+ end
@@ -0,0 +1,45 @@
1
+
2
+ require 'adapi'
3
+
4
+ require_relative 'add_bare_ad_group'
5
+
6
+ $ad = Adapi::Ad::TextAd.create( :ads => [
7
+ {
8
+ :ad_group_id => $ad_group[:id],
9
+ :headline => "Code like Neo",
10
+ :description1 => 'Need mad coding skills?',
11
+ :description2 => 'Check out my new blog!',
12
+ :url => 'http://www.demcodez.com',
13
+ :display_url => 'http://www.demcodez.com',
14
+ :status => 'PAUSED'
15
+ },
16
+
17
+ {
18
+ :ad_group_id => $ad_group[:id],
19
+ :headline => "Code like Trinity",
20
+ :description1 => 'The power of awesomeness?',
21
+ :description2 => 'Check out my new blog!',
22
+ :url => 'http://www.demcodez.com',
23
+ :display_url => 'http://www.demcodez.com',
24
+ :status => 'PAUSED'
25
+ },
26
+
27
+ {
28
+ :ad_group_id => $ad_group[:id],
29
+ :headline => "Code like Morpheus",
30
+ :description1 => 'Unleash the power of Matrix',
31
+ :description2 => 'Check out my new blog',
32
+ :url => 'http://www.demcodez.com',
33
+ :display_url => 'http://www.demcodez.com',
34
+ :status => 'PAUSED'
35
+ }
36
+ ] )
37
+
38
+ puts "\nCREATED ADS FOR AD GROUP #{$ad_group[:id]}:"
39
+
40
+ $ads = Adapi::Ad::TextAd.find( :all, :ad_group_id => $ad_group[:id] )
41
+
42
+ $ads.each_with_index do |ad, i|
43
+ puts "\nAD NR. #{i + 1}:"
44
+ pp ad.attributes
45
+ end
@@ -2,24 +2,21 @@
2
2
 
3
3
  require 'adapi'
4
4
 
5
- # This test tries to create a complete campaign and fails because ad is
6
- # intentionally left without url. The point is to test how create_campaign
7
- # fails: campaign status should be set to DELETED and name changed (so the
8
- # name isn't blocked and another campaign can be created with the same name
9
- # eventually)
5
+ # Tries to create a campaign and fails because ad is intentionally left
6
+ # without url. We test if campaign then rollbacks correctly:
7
+ # * status should be set to DELETED
8
+ # * renamed so the original campaign name isn't blocked
10
9
 
11
10
  campaign_data = {
12
11
  :name => "Campaign #%d" % (Time.new.to_f * 1000).to_i,
13
12
  :status => 'PAUSED',
14
- # Automatic CPC: BudgetOptimizer or ManualCPC
15
13
  :bidding_strategy => { :xsi_type => 'BudgetOptimizer', :bid_ceiling => 100 },
16
14
  :budget => { :amount => 50, :delivery_method => 'STANDARD' },
17
15
 
18
16
  :network_setting => {
19
17
  :target_google_search => true,
20
18
  :target_search_network => true,
21
- :target_content_network => false,
22
- :target_content_contextual => false
19
+ :target_content_network => false
23
20
  },
24
21
 
25
22
  :criteria => {
@@ -47,12 +44,38 @@ campaign_data = {
47
44
  ]
48
45
 
49
46
  }
50
-
47
+
51
48
  $campaign = Adapi::Campaign.create(campaign_data)
52
49
 
53
- p "Campaign ID #{$campaign.id} created"
54
- p "with status DELETED and changed name"
55
- pp $campaign.attributes
50
+ unless $campaign.errors.empty?
51
+
52
+ puts "\nERRORS WHEN UPDATING CAMPAIGN #{$campaign.id}:"
53
+ pp $campaign.errors.full_messages
54
+
55
+ puts "\nROLLBACKING CAMPAIGN #{$campaign.id}\n"
56
+
57
+ $campaign.rollback!
58
+
59
+ unless $campaign.errors.empty?
60
+
61
+ puts "\nERRORS WHEN ROLLBACKING CAMPAIGN #{$campaign.id}:"
62
+ pp $campaign.errors.full_messages
63
+
64
+ else
65
+
66
+ puts "\nOK, ROLLBACKED CAMPAIGN #{$campaign.id}"
67
+
68
+ $campaign = Adapi::Campaign.find($campaign.id)
69
+
70
+ puts "\nCAMPAIGN DATA:"
71
+ pp $campaign.attributes
72
+
73
+ end
74
+
75
+ else
76
+
77
+ puts "\nCREATED CAMPAIGN #{$campaign.id}"
78
+
79
+ puts "\nSOMETHING IS WRONG, THIS SHOULDN'T HAPPEN!"
56
80
 
57
- p "with errors:"
58
- pp $campaign.errors.to_a
81
+ end
@@ -12,11 +12,7 @@ $campaign = Adapi::Campaign.update(
12
12
  :network_setting => {
13
13
  :target_google_search => false,
14
14
  :target_search_network => false,
15
- :target_content_network => true,
16
- :target_content_contextual => true
17
- # FIXME returns error which is not trapped:
18
- # TargetError.CANNOT_TARGET_PARTNER_SEARCH_NETWORK
19
- # :target_partner_search_network => true
15
+ :target_content_network => true
20
16
  },
21
17
 
22
18
  # deletes all criteria (except :platform) and create these new ones
@@ -19,8 +19,7 @@ campaign_data = {
19
19
  :network_setting => {
20
20
  :target_google_search => true,
21
21
  :target_search_network => true,
22
- :target_content_network => false,
23
- :target_content_contextual => false
22
+ :target_content_network => false
24
23
  },
25
24
 
26
25
  :criteria => {
@@ -82,7 +81,7 @@ p "Created campaign ID #{$campaign.id}"
82
81
  # * change second ad_group
83
82
  # * add new ad_group
84
83
 
85
- Adapi::Campaign.update(
84
+ $campaign.update(
86
85
  :id => $campaign[:id],
87
86
  :status => 'ACTIVE',
88
87
  :name => "UPDATED #{$campaign[:name]}",
@@ -171,7 +170,7 @@ else
171
170
  puts "\nAD GROUPS (#{$ad_groups.size}):"
172
171
  $ad_groups.each_with_index do |ad_group, i|
173
172
  puts "\nAD GROUP #{i + 1}:\n"
174
- pp ad_group.attributes
173
+ pp ad_group
175
174
  end
176
175
 
177
176
  end
@@ -2,15 +2,16 @@
2
2
 
3
3
  require 'rubygems'
4
4
  require 'adwords_api'
5
- # TODO use for optional logging of general activity
6
- # currently we only load communication with adwords-api
7
- # in form of complete SOAP requests
5
+ # provides various utility methods
6
+ require 'adwords_api/utils'
7
+ # TODO log of general activity (currently log only SOAP requests)
8
8
  # require 'logger'
9
9
  require 'yaml'
10
10
  require 'pp'
11
11
 
12
12
  require 'active_model'
13
13
  # load only ActiveSupport core extensions
14
+ # TODO require only parts that are really needed
14
15
  require 'active_support/core_ext'
15
16
 
16
17
  require 'adapi/version'
@@ -44,7 +45,7 @@ HTTPI.adapter = :curb
44
45
  HTTPI.log = false # supress HTTPI output
45
46
 
46
47
  module Adapi
47
- API_VERSION = :v201109_1
48
+ API_VERSION = :v201206
48
49
  end
49
50
 
50
51
  if RUBY_VERSION < '1.9'
@@ -16,8 +16,7 @@ module Adapi
16
16
  # PS: create won't work with id and ad_group_id
17
17
  # 'id' => id, 'ad_group_id' => ad_group_id,
18
18
  def attributes
19
- super.merge( 'id' => id, 'ad_group_id' => ad_group_id,
20
- 'url' => url, 'display_url' => display_url )
19
+ super.merge( id: id, ad_group_id: ad_group_id, url: url, display_url: display_url )
21
20
  end
22
21
 
23
22
  def initialize(params = {})
@@ -31,6 +30,7 @@ module Adapi
31
30
  end
32
31
 
33
32
  # deletes ad
33
+ # TODO call by delete method
34
34
  #
35
35
  def destroy
36
36
  response = self.mutate(
@@ -44,46 +44,21 @@ module Adapi
44
44
  (response and response[:value]) ? true : false
45
45
  end
46
46
 
47
- # ad-specific mutate wrapper, deals with PolicyViolations for ads
48
- #
49
- def mutate(operation)
50
- operation = [operation] unless operation.is_a?(Array)
51
-
52
- # fix to save space during specifyng operations
53
- operation = operation.map do |op|
54
- op[:operand].delete(:status) if op[:operand][:status].nil?
55
- op
47
+ # FIXME add validations
48
+ def delete(params = {})
49
+ operations = params[:ad_ids].map do |ad_id|
50
+ {
51
+ :operator => 'REMOVE',
52
+ :operand => {
53
+ :ad_group_id => params[:ad_group_id],
54
+ :ad => { :id => ad_id, :xsi_type => 'Ad' }
55
+ }
56
+ }
56
57
  end
57
-
58
- begin
59
-
60
- response = @service.mutate(operation)
61
-
62
- rescue *API_EXCEPTIONS => e
63
58
 
64
- # return PolicyViolations in specific format so they can be sent again
65
- # see adwords-api gem example for details: handle_policy_violation_error.rb
66
- e.errors.each do |error|
67
- # error[:xsi_type] seems to be broken, so using also alternative key
68
- # also could try: :"@xsi:type" (but api_error_type seems to be more robust)
69
- if (error[:xsi_type] == 'PolicyViolationError') || (error[:api_error_type] == 'PolicyViolationError')
70
- if error[:is_exemptable]
71
- self.errors.add(:PolicyViolationError, error[:key])
72
- end
59
+ response = self.mutate(operations)
73
60
 
74
- # return also exemptable errors, operation may fail even with them
75
- self.errors.add(:base, "violated %s policy: \"%s\" on \"%s\"" % [
76
- error[:is_exemptable] ? 'exemptable' : 'non-exemptable',
77
- error[:key][:policy_name],
78
- error[:key][:violating_text]
79
- ])
80
- else
81
- self.errors.add(:base, e.message)
82
- end
83
- end # of errors.each
84
- end
85
-
86
- response
61
+ (response and response[:value]) ? true : false
87
62
  end
88
63
 
89
64
  end
@@ -1,13 +1,17 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Adapi
4
+ # http://code.google.com/apis/adwords/docs/reference/latest/AdGroupAdService.TextAd.html
5
+ #
4
6
  # Ad::TextAd == AdGroupAd::TextAd
5
7
  #
6
- # http://code.google.com/apis/adwords/docs/reference/latest/AdGroupAdService.TextAd.html
8
+ # This model supports both individual and batch create/update of ads.
9
+ # If :ads parameter is not nil, it is considered as array of ads to
10
+ # be updated in batch.
7
11
  #
8
12
  class Ad::TextAd < Ad
9
13
 
10
- ATTRIBUTES = [ :headline, :description1, :description2 ]
14
+ ATTRIBUTES = [ :headline, :description1, :description2, :ads ]
11
15
 
12
16
  attr_accessor *ATTRIBUTES
13
17
 
@@ -34,37 +38,48 @@ module Adapi
34
38
  end
35
39
 
36
40
  def create
37
- operand = self.attributes.delete_if do |k|
38
- [ :campaign_id, :ad_group_id, :id, :status ].include?(k.to_sym)
39
- end.symbolize_keys
40
-
41
- operation = {
42
- :operator => 'ADD',
43
- :operand => {
44
- :ad_group_id => @ad_group_id,
45
- :status => @status,
46
- :ad => operand
41
+ @ads = [ self.attributes ] unless @ads
42
+
43
+ operations = []
44
+
45
+ @ads.each do |ad_params|
46
+
47
+ ad = TextAd.new(ad_params)
48
+
49
+ operand = ad.attributes.delete_if do |k|
50
+ [ :campaign_id, :ad_group_id, :id, :status, :ads ].include?(k.to_sym)
51
+ end.symbolize_keys
52
+
53
+ operations << {
54
+ :operator => 'ADD',
55
+ :operand => {
56
+ :ad_group_id => ad.ad_group_id,
57
+ :status => ad.status,
58
+ :ad => operand
59
+ }
47
60
  }
48
- }
61
+ end
49
62
 
50
- response = self.mutate(operation)
63
+ self.mutate(operations)
51
64
 
52
65
  # check for PolicyViolationErrors, set exemptions and try again
53
- # TODO for now, this is only done once. how about setting a number of retries?
54
- unless self.errors[:PolicyViolationError].empty?
55
- operation[:exemption_requests] = self.errors[:PolicyViolationError].map do |error_key|
56
- { :key => error_key }
66
+ # do it only once. from my experience with AdWords API, multiple retries are bad practice
67
+ if self.errors[:PolicyViolationError].any?
68
+
69
+ self.errors[:PolicyViolationError].each do |e|
70
+ i = e.delete(:operation_index)
71
+ operations[i][:exemption_requests] = [ { :key => e } ]
57
72
  end
58
73
 
59
74
  self.errors.clear
60
75
 
61
- response = self.mutate(operation)
76
+ self.mutate(operations)
62
77
  end
63
78
 
64
- return false unless self.errors.empty?
65
-
66
- # set ad id
67
- self.id = response[:value].first[:ad][:id] rescue nil
79
+ return false if self.errors.any?
80
+
81
+ # FIXME return ids of newly created ads
82
+ # self.id = response[:value].first[:ad][:id] rescue nil
68
83
 
69
84
  true
70
85
  end
@@ -50,7 +50,7 @@ module Adapi
50
50
  end
51
51
 
52
52
  # convert bid amounts to micro_amounts
53
- [ :proxy_keyword_max_cpc, :proxy_site_max_cpc ].each do |k|
53
+ [ :proxy_keyword_max_cpc ].each do |k|
54
54
  if @bids[k] and not @bids[k].is_a?(Hash)
55
55
  @bids[k] = {
56
56
  :amount => {
@@ -76,7 +76,7 @@ module Adapi
76
76
  :operand => operand
77
77
  )
78
78
 
79
- return false unless (response and response[:value])
79
+ check_for_errors(self)
80
80
 
81
81
  self.id = response[:value].first[:id] rescue nil
82
82
 
@@ -86,22 +86,19 @@ module Adapi
86
86
  :keywords => @keywords
87
87
  )
88
88
 
89
- if (keyword.errors.size > 0)
90
- self.errors.add("[keyword]", keyword.errors.to_a)
91
- return false
92
- end
89
+ check_for_errors(keyword, :prefix => "Keyword")
93
90
  end
94
91
 
95
- @ads.each do |ad_data|
96
- ad = Adapi::Ad::TextAd.create( ad_data.merge(:ad_group_id => @id) )
92
+ if not @ads.empty?
93
+ ad = Adapi::Ad::TextAd.create( :ads => @ads.map { |ad_data| ad_data.merge(:ad_group_id => @id) } )
97
94
 
98
- if (ad.errors.size > 0)
99
- self.errors.add("[ad] \"#{ad.headline}\"", ad.errors.to_a)
100
- return false
101
- end
95
+ check_for_errors(ad, :prefix => "Ad \"#{ad.headline}\"")
102
96
  end
103
97
 
104
- true
98
+ self.errors.empty?
99
+
100
+ rescue AdGroupError => e
101
+ false
105
102
  end
106
103
 
107
104
  def update(params = {})
@@ -120,7 +117,7 @@ module Adapi
120
117
  :operand => core_params.merge( :id => @id, :campaign_id => @campaign_id )
121
118
  )
122
119
 
123
- return false unless (response and response[:value])
120
+ check_for_errors(ad_group, :store_errors => false)
124
121
 
125
122
  # step 2. update keywords
126
123
  # delete everything and create new keywords
@@ -136,36 +133,45 @@ module Adapi
136
133
  :ad_group_id => @id,
137
134
  :keywords => params[:keywords]
138
135
  )
139
-
140
- unless result.errors.empty?
141
- self.errors.add("Keyword", result.errors.to_a)
142
- return false
143
- end
136
+
137
+ check_for_errors(result, :prefix => "Keyword")
144
138
  end
145
139
 
146
140
  # step 3. update ads
147
141
  # ads can't be updated, gotta remove them all and add new ads
148
142
  if params[:ads] and not params[:ads].empty?
149
143
  # remove all existing ads
144
+ # TODO change into class method
145
+ existing_ads = self.find_ads
146
+ ad = existing_ads.first.delete(
147
+ :ad_group_id => @id,
148
+ :ad_ids => existing_ads.map { |ad| ad.id }
149
+ )
150
+
151
+ unless ad
152
+ # REFACTOR
153
+ self.errors.add(:base, add.errors.full_messages)
154
+ return false
155
+ end
156
+
157
+ =begin
150
158
  self.find_ads.each do |ad|
151
159
  unless ad.destroy
152
160
  self.errors.add("Ad \"#{ad.headline}\"", ["cannot be deleted"])
153
161
  return false
154
162
  end
155
163
  end
156
-
164
+ =end
157
165
  # create new ads
158
- params[:ads].each do |ad|
159
- ad = Adapi::Ad::TextAd.create( ad.merge(:ad_group_id => @id) )
166
+ ad = Adapi::Ad::TextAd.create( :ads => params[:ads].map { |ad_data| ad_data.merge(:ad_group_id => @id) } )
160
167
 
161
- unless ad.errors.empty?
162
- self.errors.add("Ad \"#{ad.headline}\"", ad.errors.to_a)
163
- return false
164
- end
165
- end
168
+ check_for_errors(ad, :prefix => "Ad \"#{ad.headline}\"")
166
169
  end
167
170
 
168
- true
171
+ self.errors.empty?
172
+
173
+ rescue AdGroupError => e
174
+ false
169
175
  end
170
176
 
171
177
  # PS: perhaps also change the ad_group name when deleting
@@ -189,8 +195,7 @@ module Adapi
189
195
 
190
196
  select_fields = %w{ Id CampaignId Name Status }
191
197
  # add Bids atributes
192
- select_fields += %w{ EnhancedCpcEnabled
193
- ProxyKeywordMaxCpc ProxySiteMaxCpc
198
+ select_fields += %w{ EnhancedCpcEnabled ProxyKeywordMaxCpc
194
199
  KeywordMaxCpc KeywordContentMaxCpc }
195
200
 
196
201
  selector = {