adapi 0.0.9 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +35 -27
- data/adapi.gemspec +8 -8
- data/examples/add_ad_group.rb +13 -7
- data/examples/add_bare_ad_group.rb +12 -6
- data/examples/add_bare_campaign.rb +13 -6
- data/examples/add_campaign.rb +22 -10
- data/examples/add_campaign_criteria.rb +22 -8
- data/examples/add_invalid_ad_group.rb +9 -5
- data/examples/add_invalid_keywords.rb +3 -0
- data/examples/add_invalid_text_ad.rb +12 -6
- data/examples/add_keywords.rb +20 -9
- data/examples/add_text_ads.rb +45 -0
- data/examples/rollback_campaign.rb +37 -14
- data/examples/update_campaign.rb +1 -5
- data/examples/update_complete_campaign.rb +3 -4
- data/lib/adapi.rb +5 -4
- data/lib/adapi/ad.rb +14 -39
- data/lib/adapi/ad/text_ad.rb +38 -23
- data/lib/adapi/ad_group.rb +35 -30
- data/lib/adapi/ad_group_criterion.rb +7 -4
- data/lib/adapi/ad_param.rb +8 -16
- data/lib/adapi/api.rb +88 -8
- data/lib/adapi/campaign.rb +87 -76
- data/lib/adapi/campaign_criterion.rb +6 -4
- data/lib/adapi/campaign_target.rb +6 -4
- data/lib/adapi/config.rb +2 -2
- data/lib/adapi/keyword.rb +11 -58
- data/lib/adapi/version.rb +7 -1
- data/test/integration/create_ad_group_test.rb +48 -0
- data/test/integration/create_campaign_test.rb +1 -4
- data/test/test_helper.rb +49 -0
- metadata +43 -41
data/examples/add_keywords.rb
CHANGED
@@ -10,17 +10,28 @@ $keywords = Adapi::Keyword.new(
|
|
10
10
|
:keywords => [ 'dem codez', '"top coder"', '[-code]' ]
|
11
11
|
)
|
12
12
|
|
13
|
-
$
|
13
|
+
$keywords.create
|
14
14
|
|
15
|
-
|
16
|
-
$google_keywords = Adapi::Keyword.find(:all, :ad_group_id => $ad_group[:id]).keywords
|
15
|
+
if $keywords.errors.any?
|
17
16
|
|
18
|
-
|
17
|
+
puts "\nERRORS:"
|
18
|
+
pp $keywords.errors.messages
|
19
19
|
|
20
|
-
|
20
|
+
else
|
21
21
|
|
22
|
-
|
23
|
-
pp $params_keywords
|
22
|
+
puts "\nKEYWORDS CREATED\n"
|
24
23
|
|
25
|
-
|
26
|
-
|
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
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
58
|
-
pp $campaign.errors.to_a
|
81
|
+
end
|
data/examples/update_campaign.rb
CHANGED
@@ -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
|
-
|
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
|
173
|
+
pp ad_group
|
175
174
|
end
|
176
175
|
|
177
176
|
end
|
data/lib/adapi.rb
CHANGED
@@ -2,15 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
4
|
require 'adwords_api'
|
5
|
-
#
|
6
|
-
|
7
|
-
#
|
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 = :
|
48
|
+
API_VERSION = :v201206
|
48
49
|
end
|
49
50
|
|
50
51
|
if RUBY_VERSION < '1.9'
|
data/lib/adapi/ad.rb
CHANGED
@@ -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(
|
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
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/adapi/ad/text_ad.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
:
|
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
|
-
|
63
|
+
self.mutate(operations)
|
51
64
|
|
52
65
|
# check for PolicyViolationErrors, set exemptions and try again
|
53
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
76
|
+
self.mutate(operations)
|
62
77
|
end
|
63
78
|
|
64
|
-
return false
|
65
|
-
|
66
|
-
#
|
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
|
data/lib/adapi/ad_group.rb
CHANGED
@@ -50,7 +50,7 @@ module Adapi
|
|
50
50
|
end
|
51
51
|
|
52
52
|
# convert bid amounts to micro_amounts
|
53
|
-
[ :proxy_keyword_max_cpc
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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].
|
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
|
-
|
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
|
-
|
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 = {
|