adapi 0.0.9 → 0.1.0

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