adapi 0.0.8 → 0.0.9
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.
- data/.gitignore +1 -1
- data/README.markdown +2 -1
- data/adapi.gemspec +11 -11
- data/examples/add_ad_group.rb +0 -0
- data/examples/add_bare_ad_group.rb +3 -1
- data/examples/add_bare_campaign.rb +6 -4
- data/examples/add_campaign.rb +10 -3
- data/examples/add_campaign_criteria.rb +5 -5
- data/examples/add_invalid_ad_group.rb +4 -6
- data/examples/add_invalid_keywords.rb +40 -0
- data/examples/add_invalid_text_ad.rb +6 -6
- data/examples/add_keywords.rb +0 -0
- data/examples/add_negative_campaign_criteria.rb +0 -0
- data/examples/add_text_ad.rb +5 -9
- data/examples/custom_settings.yml +8 -0
- data/examples/customize_configuration.rb +0 -0
- data/examples/delete_ad_group.rb +11 -0
- data/examples/delete_keyword.rb +0 -0
- data/examples/delete_text_ad.rb +13 -0
- data/examples/find_ad_group.rb +10 -0
- data/examples/find_all_campaigns.rb +0 -0
- data/examples/find_bare_campaign.rb +34 -0
- data/examples/find_campaign.rb +0 -0
- data/examples/find_campaign_ad_groups.rb +0 -0
- data/examples/find_campaign_criteria.rb +0 -0
- data/examples/find_locations.rb +0 -0
- data/examples/log_to_specific_account.rb +0 -0
- data/examples/rollback_campaign.rb +0 -0
- data/examples/test_diacritics.rb +0 -0
- data/examples/update_ad_group.rb +70 -0
- data/examples/update_campaign.rb +34 -9
- data/examples/update_campaign_ad_groups.rb +137 -0
- data/examples/update_campaign_criteria.rb +33 -0
- data/examples/update_complete_campaign.rb +177 -0
- data/examples/update_text_ad.rb +18 -0
- data/lib/adapi/ad/text_ad.rb +39 -49
- data/lib/adapi/ad.rb +30 -19
- data/lib/adapi/ad_group.rb +110 -30
- data/lib/adapi/api.rb +25 -28
- data/lib/adapi/campaign.rb +216 -70
- data/lib/adapi/campaign_criterion.rb +44 -5
- data/lib/adapi/config.rb +1 -4
- data/lib/adapi/constant_data/language.rb +11 -2
- data/lib/adapi/keyword.rb +60 -1
- data/lib/adapi/version.rb +11 -2
- data/lib/adapi.rb +10 -7
- data/test/{config/adapi.yml.template → fixtures/adapi.yml} +11 -2
- data/test/{config/adwords_api.yml.template → fixtures/adwords_api.yml} +6 -2
- data/test/integration/find_location_test.rb +1 -1
- data/test/unit/ad_group_test.rb +2 -2
- data/test/unit/config_test.rb +8 -27
- data/test/unit/constant_data/language_test.rb +34 -0
- metadata +52 -108
- data/examples/update_campaign_status.rb +0 -15
- data/lib/savon_monkeypatch.rb +0 -43
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'adapi'
|
4
|
+
|
5
|
+
require_relative 'add_campaign_criteria'
|
6
|
+
|
7
|
+
=begin this is an obsolete way to do this
|
8
|
+
Adapi::CampaignCriterion.new(
|
9
|
+
:campaign_id => $campaign[:id],
|
10
|
+
:criteria => {
|
11
|
+
:language => %w{ en cs},
|
12
|
+
}
|
13
|
+
).destroy
|
14
|
+
|
15
|
+
Adapi::CampaignCriterion.new(
|
16
|
+
:campaign_id => $campaign[:id],
|
17
|
+
:criteria => {
|
18
|
+
:language => %w{ sk },
|
19
|
+
}
|
20
|
+
).create
|
21
|
+
=end
|
22
|
+
|
23
|
+
$new_criteria = Adapi::CampaignCriterion.new(
|
24
|
+
:campaign_id => $campaign[:id],
|
25
|
+
:criteria => {
|
26
|
+
:language => %w{ sk },
|
27
|
+
}
|
28
|
+
)
|
29
|
+
|
30
|
+
$result = $new_criteria.update!
|
31
|
+
|
32
|
+
$campaign_criterion = Adapi::CampaignCriterion.find( :campaign_id => $campaign[:id] )
|
33
|
+
pp $campaign_criterion
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'adapi'
|
4
|
+
|
5
|
+
# create campaign by single command, with campaing targets, with ad_groups
|
6
|
+
# including keywords and ads
|
7
|
+
|
8
|
+
$ad_group_names = [
|
9
|
+
"AdGroup 01 #%d" % (Time.new.to_f * 1000).to_i,
|
10
|
+
"AdGroup 02 #%d" % (Time.new.to_f * 1000).to_i
|
11
|
+
]
|
12
|
+
|
13
|
+
campaign_data = {
|
14
|
+
# basic data for campaign
|
15
|
+
:name => "Campaign #%d" % (Time.new.to_f * 1000).to_i,
|
16
|
+
:status => 'PAUSED',
|
17
|
+
:bidding_strategy => 'ManualCPC',
|
18
|
+
:budget => 50,
|
19
|
+
:network_setting => {
|
20
|
+
:target_google_search => true,
|
21
|
+
:target_search_network => true,
|
22
|
+
:target_content_network => false,
|
23
|
+
:target_content_contextual => false
|
24
|
+
},
|
25
|
+
|
26
|
+
:criteria => {
|
27
|
+
:language => [ :en, :cs ],
|
28
|
+
:geo => { :proximity => { :geo_point => '38.89859,-77.035971', :radius => '10 km' } }
|
29
|
+
},
|
30
|
+
|
31
|
+
:ad_groups => [
|
32
|
+
{
|
33
|
+
:name => $ad_group_names[0],
|
34
|
+
:status => 'ENABLED',
|
35
|
+
|
36
|
+
:keywords => [ 'neo', 'dem codez', '"top coder"', "[-code]" ],
|
37
|
+
|
38
|
+
:ads => [
|
39
|
+
{
|
40
|
+
:headline => "Code like Neo",
|
41
|
+
:description1 => 'Need mad coding skills?',
|
42
|
+
:description2 => 'Check out my new blog!',
|
43
|
+
:url => 'http://www.demcodez.com',
|
44
|
+
:display_url => 'http://www.demcodez.com'
|
45
|
+
}
|
46
|
+
]
|
47
|
+
},
|
48
|
+
|
49
|
+
{
|
50
|
+
:name => $ad_group_names[1],
|
51
|
+
:status => 'PAUSED',
|
52
|
+
|
53
|
+
:keywords => [ 'dem codez', 'trinity', 'morpheus', '"top coder"', "[-code]" ],
|
54
|
+
|
55
|
+
:ads => [
|
56
|
+
{
|
57
|
+
:headline => "Code like Trinity",
|
58
|
+
:description1 => 'The power of awesomeness?',
|
59
|
+
:description2 => 'Check out my new blog!',
|
60
|
+
:url => 'http://www.demcodez.com',
|
61
|
+
:display_url => 'http://www.demcodez.com'
|
62
|
+
},
|
63
|
+
|
64
|
+
{
|
65
|
+
:headline => "Code like Morpheus",
|
66
|
+
:description1 => 'Unleash the power of Matrix',
|
67
|
+
:description2 => 'Check out my new blog',
|
68
|
+
:url => 'http://www.demcodez.com',
|
69
|
+
:display_url => 'http://www.demcodez.com'
|
70
|
+
}
|
71
|
+
]
|
72
|
+
}
|
73
|
+
]
|
74
|
+
|
75
|
+
}
|
76
|
+
|
77
|
+
$campaign = Adapi::Campaign.create(campaign_data)
|
78
|
+
p "Created campaign ID #{$campaign.id}"
|
79
|
+
|
80
|
+
# PS: changes in ad_groups:
|
81
|
+
# * delete first ad_group
|
82
|
+
# * change second ad_group
|
83
|
+
# * add new ad_group
|
84
|
+
|
85
|
+
Adapi::Campaign.update(
|
86
|
+
:id => $campaign[:id],
|
87
|
+
:status => 'ACTIVE',
|
88
|
+
:name => "UPDATED #{$campaign[:name]}",
|
89
|
+
:bidding_strategy => {
|
90
|
+
:xsi_type => 'BudgetOptimizer',
|
91
|
+
:bid_ceiling => 20
|
92
|
+
},
|
93
|
+
:budget => 75,
|
94
|
+
|
95
|
+
# deletes all criteria (except :platform) and create these new ones
|
96
|
+
:criteria => {
|
97
|
+
:language => [ :sk ],
|
98
|
+
},
|
99
|
+
|
100
|
+
:ad_groups => [
|
101
|
+
# no match here for $ad_group_names[0], so it's going to be deleted
|
102
|
+
|
103
|
+
# this ad_group will be created
|
104
|
+
{
|
105
|
+
:name => "UPDATED " + $ad_group_names[0],
|
106
|
+
:status => 'ENABLED',
|
107
|
+
|
108
|
+
:keywords => [ 'neo update', 'dem codezzz', '"top coder"' ],
|
109
|
+
|
110
|
+
:ads => [
|
111
|
+
{
|
112
|
+
:headline => "Update like Neo",
|
113
|
+
:description1 => 'Need mad coding skills?',
|
114
|
+
:description2 => 'Check out my new blog!',
|
115
|
+
:url => 'http://www.demcodez.com',
|
116
|
+
:display_url => 'http://www.demcodez.com'
|
117
|
+
}
|
118
|
+
]
|
119
|
+
},
|
120
|
+
|
121
|
+
# this ad_group is going to be updated
|
122
|
+
{
|
123
|
+
:name => $ad_group_names[1],
|
124
|
+
:status => 'ENABLED', # from PAUSED
|
125
|
+
|
126
|
+
:keywords => [ 'dem updatez', 'update trinity', 'update morpheus' ],
|
127
|
+
|
128
|
+
:ads => [
|
129
|
+
{
|
130
|
+
:headline => "Update like Trinity",
|
131
|
+
:description1 => 'The power of updates?',
|
132
|
+
:description2 => 'Check out my new blog!',
|
133
|
+
:url => 'http://www.demcodez.com',
|
134
|
+
:display_url => 'http://www.demcodez.com'
|
135
|
+
},
|
136
|
+
|
137
|
+
{
|
138
|
+
:headline => "Update like Morpheus",
|
139
|
+
:description1 => 'Unleash the power of updates',
|
140
|
+
:description2 => 'Check out my new blog',
|
141
|
+
:url => 'http://www.demcodez.com',
|
142
|
+
:display_url => 'http://www.demcodez.com'
|
143
|
+
}
|
144
|
+
]
|
145
|
+
}
|
146
|
+
]
|
147
|
+
)
|
148
|
+
|
149
|
+
unless $campaign.errors.empty?
|
150
|
+
|
151
|
+
puts "ERROR WHEN UPDATING AD GROUPS:"
|
152
|
+
puts $campaign.errors.full_messages.join("\n")
|
153
|
+
|
154
|
+
else
|
155
|
+
|
156
|
+
# reload campaign
|
157
|
+
$campaign = Adapi::Campaign.find_complete($campaign.id)
|
158
|
+
|
159
|
+
$campaign_attributes = $campaign.attributes
|
160
|
+
$criteria = $campaign_attributes.delete(:criteria)
|
161
|
+
$ad_groups = $campaign_attributes.delete(:ad_groups)
|
162
|
+
|
163
|
+
puts "\nCAMPAIGN UPDATED\n"
|
164
|
+
|
165
|
+
puts "\nCAMPAIGN DATA:"
|
166
|
+
pp $campaign_attributes
|
167
|
+
|
168
|
+
puts "\nCAMPAIGN CRITERIA:"
|
169
|
+
pp $criteria
|
170
|
+
|
171
|
+
puts "\nAD GROUPS (#{$ad_groups.size}):"
|
172
|
+
$ad_groups.each_with_index do |ad_group, i|
|
173
|
+
puts "\nAD GROUP #{i + 1}:\n"
|
174
|
+
pp ad_group.attributes
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
|
2
|
+
require 'adapi'
|
3
|
+
|
4
|
+
require_relative 'add_text_ad'
|
5
|
+
|
6
|
+
$new_ad = $ad.update(
|
7
|
+
:status => 'ACTIVE',
|
8
|
+
:headline => "Code like Trinity",
|
9
|
+
:description1 => 'Need mad update skills?',
|
10
|
+
:description2 => 'Check out my updates!',
|
11
|
+
:url => 'http://www.demupdatez.com',
|
12
|
+
:display_url => 'http://www.demupdatez.com'
|
13
|
+
)
|
14
|
+
|
15
|
+
$new_ad = Adapi::Ad::TextAd.find(:first, :ad_group_id => $new_ad.ad_group_id, :id => $new_ad.id )
|
16
|
+
|
17
|
+
puts "\nUPDATED (AND RELOADED) AD:"
|
18
|
+
pp $new_ad.attributes
|
data/lib/adapi/ad/text_ad.rb
CHANGED
@@ -7,19 +7,23 @@ module Adapi
|
|
7
7
|
#
|
8
8
|
class Ad::TextAd < Ad
|
9
9
|
|
10
|
-
|
10
|
+
ATTRIBUTES = [ :headline, :description1, :description2 ]
|
11
|
+
|
12
|
+
attr_accessor *ATTRIBUTES
|
11
13
|
|
12
14
|
def attributes
|
13
|
-
super.merge
|
15
|
+
super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
|
14
16
|
end
|
15
17
|
|
18
|
+
alias to_hash attributes
|
19
|
+
|
16
20
|
def initialize(params = {})
|
17
21
|
params[:service_name] = :AdGroupAdService
|
18
22
|
|
19
23
|
@xsi_type = 'TextAd'
|
20
24
|
|
21
|
-
|
22
|
-
self.send
|
25
|
+
ATTRIBUTES.each do |param_name|
|
26
|
+
self.send("#{param_name}=", params[param_name])
|
23
27
|
end
|
24
28
|
|
25
29
|
super(params)
|
@@ -30,57 +34,58 @@ module Adapi
|
|
30
34
|
end
|
31
35
|
|
32
36
|
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
|
+
|
33
41
|
operation = {
|
34
42
|
:operator => 'ADD',
|
35
43
|
:operand => {
|
36
44
|
:ad_group_id => @ad_group_id,
|
37
45
|
:status => @status,
|
38
|
-
:ad =>
|
46
|
+
:ad => operand
|
39
47
|
}
|
40
48
|
}
|
41
49
|
|
42
50
|
response = self.mutate(operation)
|
43
|
-
|
44
|
-
# check for
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
{ :key => error }
|
51
|
+
|
52
|
+
# 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 }
|
50
57
|
end
|
51
58
|
|
52
59
|
self.errors.clear
|
53
60
|
|
54
|
-
response = self.mutate(operation)
|
61
|
+
response = self.mutate(operation)
|
55
62
|
end
|
56
63
|
|
57
|
-
return false unless
|
58
|
-
|
64
|
+
return false unless self.errors.empty?
|
65
|
+
|
66
|
+
# set ad id
|
59
67
|
self.id = response[:value].first[:ad][:id] rescue nil
|
60
68
|
|
61
69
|
true
|
62
70
|
end
|
63
71
|
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
72
|
+
# except for status, we cannot edit ad fields
|
73
|
+
# gotta delete an ad and create a new one instead
|
74
|
+
# this means that this method returns new ad id!
|
75
|
+
#
|
76
|
+
# REFACTOR this method shpould be removed (but that might break something,
|
77
|
+
# os let's keep it here for the moment)
|
67
78
|
#
|
68
79
|
def update(params = {})
|
69
|
-
# set
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
:operator => 'SET',
|
76
|
-
:operand => {
|
77
|
-
:ad_group_id => self.ad_group_id,
|
78
|
-
:ad => updated_params.merge(:id => self.id),
|
79
|
-
:status => updated_status
|
80
|
-
}
|
81
|
-
)
|
80
|
+
# set attributes for the "updated" ad
|
81
|
+
create_attributes = self.attributes.merge(params).symbolize_keys
|
82
|
+
create_attributes.delete(:id)
|
83
|
+
|
84
|
+
# delete current ad
|
85
|
+
return false unless self.destroy
|
82
86
|
|
83
|
-
|
87
|
+
# create new add
|
88
|
+
TextAd.create(create_attributes)
|
84
89
|
end
|
85
90
|
|
86
91
|
def find # == refresh
|
@@ -101,13 +106,12 @@ module Adapi
|
|
101
106
|
# supported condition parameters: ad_group_id and id
|
102
107
|
predicates = [ :ad_group_id, :id ].map do |param_name|
|
103
108
|
if params[param_name]
|
104
|
-
|
105
|
-
{:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
|
109
|
+
{ :field => param_name.to_s.camelcase, :operator => 'IN', :values => Array( params[param_name] ) }
|
106
110
|
end
|
107
111
|
end.compact
|
108
112
|
|
109
113
|
selector = {
|
110
|
-
:fields => ['Id', 'Headline'],
|
114
|
+
:fields => ['Id', 'AdGroupId', 'Headline' ],
|
111
115
|
:ordering => [{:field => 'Id', :sort_order => 'ASCENDING'}],
|
112
116
|
:predicates => predicates
|
113
117
|
}
|
@@ -121,22 +125,8 @@ module Adapi
|
|
121
125
|
end
|
122
126
|
|
123
127
|
# TODO convert to TextAd instances
|
124
|
-
# PS: we already have ad_group_id parameter
|
125
128
|
first_only ? response.first : response
|
126
129
|
end
|
127
130
|
|
128
|
-
# Converts text ad data to hash - of the same structure which is used when
|
129
|
-
# creating a complete campaign.
|
130
|
-
#
|
131
|
-
def to_hash
|
132
|
-
{
|
133
|
-
:headline => self[:headline],
|
134
|
-
:description1 => self[:description1],
|
135
|
-
:description2 => self[:description2],
|
136
|
-
:url => self[:url],
|
137
|
-
:display_url => self[:display_url]
|
138
|
-
}
|
139
|
-
end
|
140
|
-
|
141
131
|
end
|
142
132
|
end
|
data/lib/adapi/ad.rb
CHANGED
@@ -6,7 +6,9 @@ module Adapi
|
|
6
6
|
# wraps all types of ads: text ads, image ads...
|
7
7
|
class Ad < Api
|
8
8
|
|
9
|
-
|
9
|
+
# REFACTOR attributes
|
10
|
+
|
11
|
+
attr_accessor :id, :ad_group_id, :url, :display_url, :approval_status,
|
10
12
|
:disapproval_reasons, :trademark_disapproved
|
11
13
|
|
12
14
|
validates_presence_of :ad_group_id
|
@@ -14,7 +16,8 @@ module Adapi
|
|
14
16
|
# PS: create won't work with id and ad_group_id
|
15
17
|
# 'id' => id, 'ad_group_id' => ad_group_id,
|
16
18
|
def attributes
|
17
|
-
super.merge('
|
19
|
+
super.merge( 'id' => id, 'ad_group_id' => ad_group_id,
|
20
|
+
'url' => url, 'display_url' => display_url )
|
18
21
|
end
|
19
22
|
|
20
23
|
def initialize(params = {})
|
@@ -33,15 +36,14 @@ module Adapi
|
|
33
36
|
response = self.mutate(
|
34
37
|
:operator => 'REMOVE',
|
35
38
|
:operand => {
|
36
|
-
:ad_group_id =>
|
37
|
-
:ad => { :id =>
|
39
|
+
:ad_group_id => @ad_group_id,
|
40
|
+
:ad => { :id => @id, :xsi_type => 'Ad' }
|
38
41
|
}
|
39
42
|
)
|
40
43
|
|
41
44
|
(response and response[:value]) ? true : false
|
42
45
|
end
|
43
46
|
|
44
|
-
|
45
47
|
# ad-specific mutate wrapper, deals with PolicyViolations for ads
|
46
48
|
#
|
47
49
|
def mutate(operation)
|
@@ -53,25 +55,34 @@ module Adapi
|
|
53
55
|
op
|
54
56
|
end
|
55
57
|
|
56
|
-
begin
|
58
|
+
begin
|
59
|
+
|
57
60
|
response = @service.mutate(operation)
|
58
|
-
|
59
|
-
rescue AdsCommon::Errors::HttpError => e
|
60
|
-
self.errors.add(:base, e.message)
|
61
61
|
|
62
|
-
|
63
|
-
|
64
|
-
# return PolicyViolations so they can be sent again
|
62
|
+
rescue *API_EXCEPTIONS => e
|
63
|
+
|
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
|
65
66
|
e.errors.each do |error|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
73
|
+
|
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)
|
71
82
|
end
|
72
|
-
end
|
83
|
+
end # of errors.each
|
73
84
|
end
|
74
|
-
|
85
|
+
|
75
86
|
response
|
76
87
|
end
|
77
88
|
|
data/lib/adapi/ad_group.rb
CHANGED
@@ -1,31 +1,49 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# This class handles operations with ad_groups
|
4
|
+
#
|
5
|
+
# https://developers.google.com/adwords/api/docs/reference/latest/AdGroupService
|
6
|
+
#
|
3
7
|
module Adapi
|
4
8
|
class AdGroup < Api
|
5
9
|
|
6
|
-
|
10
|
+
ATTRIBUTES = [ :id, :campaign_id, :name, :status, :bids, :keywords, :ads ]
|
11
|
+
|
12
|
+
attr_accessor *ATTRIBUTES
|
7
13
|
|
8
14
|
validates_presence_of :campaign_id, :name, :status
|
9
15
|
validates_inclusion_of :status, :in => %w{ ENABLED PAUSED DELETED }
|
10
16
|
|
11
17
|
def attributes
|
12
|
-
super.merge
|
18
|
+
super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
|
13
19
|
end
|
14
20
|
|
21
|
+
alias to_hash attributes
|
22
|
+
|
15
23
|
def initialize(params = {})
|
16
24
|
params[:service_name] = :AdGroupService
|
17
25
|
|
18
26
|
@xsi_type = 'AdGroup'
|
19
27
|
|
20
|
-
|
21
|
-
self.send
|
28
|
+
ATTRIBUTES.each do |param_name|
|
29
|
+
self.send("#{param_name}=", params[param_name])
|
22
30
|
end
|
23
31
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
32
|
+
@keywords ||= []
|
33
|
+
|
34
|
+
@ads ||= []
|
35
|
+
|
36
|
+
super(params)
|
37
|
+
end
|
38
|
+
|
39
|
+
# convert bids to GoogleApi format
|
40
|
+
#
|
41
|
+
# can be either string (just xsi_type) or hash (xsi_type with params)
|
42
|
+
# although I'm not sure if just string makes sense in this case
|
43
|
+
#
|
44
|
+
def bids=(params = {})
|
45
|
+
@bids = params
|
46
|
+
|
29
47
|
if @bids
|
30
48
|
unless @bids.is_a?(Hash)
|
31
49
|
@bids = { :xsi_type => @bids }
|
@@ -42,11 +60,6 @@ module Adapi
|
|
42
60
|
end
|
43
61
|
end
|
44
62
|
end
|
45
|
-
|
46
|
-
@keywords ||= []
|
47
|
-
@ads ||= []
|
48
|
-
|
49
|
-
super(params)
|
50
63
|
end
|
51
64
|
|
52
65
|
def create
|
@@ -90,22 +103,98 @@ module Adapi
|
|
90
103
|
|
91
104
|
true
|
92
105
|
end
|
106
|
+
|
107
|
+
def update(params = {})
|
108
|
+
# step 1. update core attributes
|
109
|
+
core_attributes = [ :id, :campaign_id, :name, :status, :bids ]
|
110
|
+
# get operand in google format
|
111
|
+
# parse the given params by initialize method...
|
112
|
+
ad_group = Adapi::AdGroup.new(params)
|
113
|
+
# HOTFIX remove :service_name param inserted by initialize method
|
114
|
+
params.delete(:service_name)
|
115
|
+
# ...and load parsed params back into the hash
|
116
|
+
core_params = Hash[ core_attributes.map { |k| [k, ad_group.send(k)] if params[k].present? }.compact ]
|
117
|
+
|
118
|
+
response = ad_group.mutate(
|
119
|
+
:operator => 'SET',
|
120
|
+
:operand => core_params.merge( :id => @id, :campaign_id => @campaign_id )
|
121
|
+
)
|
122
|
+
|
123
|
+
return false unless (response and response[:value])
|
124
|
+
|
125
|
+
# step 2. update keywords
|
126
|
+
# delete everything and create new keywords
|
127
|
+
if params[:keywords] and not params[:keywords].empty?
|
128
|
+
# delete existing keywords
|
129
|
+
# OPTIMIZE should be all in one request
|
130
|
+
Keyword.find(:all, :ad_group_id => @id).keywords.each do |keyword|
|
131
|
+
Keyword.new(:ad_group_id => @id).delete(keyword[:text][:criterion][:id])
|
132
|
+
end
|
133
|
+
|
134
|
+
# create new keywords
|
135
|
+
result = Adapi::Keyword.create(
|
136
|
+
:ad_group_id => @id,
|
137
|
+
:keywords => params[:keywords]
|
138
|
+
)
|
139
|
+
|
140
|
+
unless result.errors.empty?
|
141
|
+
self.errors.add("Keyword", result.errors.to_a)
|
142
|
+
return false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# step 3. update ads
|
147
|
+
# ads can't be updated, gotta remove them all and add new ads
|
148
|
+
if params[:ads] and not params[:ads].empty?
|
149
|
+
# remove all existing ads
|
150
|
+
self.find_ads.each do |ad|
|
151
|
+
unless ad.destroy
|
152
|
+
self.errors.add("Ad \"#{ad.headline}\"", ["cannot be deleted"])
|
153
|
+
return false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# create new ads
|
158
|
+
params[:ads].each do |ad|
|
159
|
+
ad = Adapi::Ad::TextAd.create( ad.merge(:ad_group_id => @id) )
|
160
|
+
|
161
|
+
unless ad.errors.empty?
|
162
|
+
self.errors.add("Ad \"#{ad.headline}\"", ad.errors.to_a)
|
163
|
+
return false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
true
|
169
|
+
end
|
93
170
|
|
171
|
+
# PS: perhaps also change the ad_group name when deleting
|
172
|
+
def delete
|
173
|
+
update(:status => 'DELETED')
|
174
|
+
end
|
175
|
+
|
94
176
|
def self.find(amount = :all, params = {})
|
95
177
|
params.symbolize_keys!
|
96
178
|
first_only = (amount.to_sym == :first)
|
179
|
+
# by default, exclude ad_groups with status DELETED
|
180
|
+
params[:status] ||= %w{ ENABLED PAUSED }
|
97
181
|
|
98
182
|
raise "Campaign ID is required" unless params[:campaign_id]
|
99
183
|
|
100
|
-
predicates = [ :campaign_id, :id ].map do |param_name|
|
101
|
-
if params[param_name]
|
102
|
-
|
103
|
-
{:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
|
184
|
+
predicates = [ :campaign_id, :id, :name, :status ].map do |param_name|
|
185
|
+
if params[param_name].present?
|
186
|
+
{:field => param_name.to_s.camelcase, :operator => 'IN', :values => Array( params[param_name] ) }
|
104
187
|
end
|
105
188
|
end.compact
|
106
189
|
|
190
|
+
select_fields = %w{ Id CampaignId Name Status }
|
191
|
+
# add Bids atributes
|
192
|
+
select_fields += %w{ EnhancedCpcEnabled
|
193
|
+
ProxyKeywordMaxCpc ProxySiteMaxCpc
|
194
|
+
KeywordMaxCpc KeywordContentMaxCpc }
|
195
|
+
|
107
196
|
selector = {
|
108
|
-
:fields =>
|
197
|
+
:fields => select_fields,
|
109
198
|
:ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
|
110
199
|
:predicates => predicates
|
111
200
|
}
|
@@ -124,6 +213,8 @@ module Adapi
|
|
124
213
|
)
|
125
214
|
end
|
126
215
|
|
216
|
+
ad_groups.map! { |ad_group| AdGroup.new(ad_group) }
|
217
|
+
|
127
218
|
first_only ? ad_groups.first : ad_groups
|
128
219
|
end
|
129
220
|
|
@@ -136,16 +227,5 @@ module Adapi
|
|
136
227
|
Ad::TextAd.find( (first_only ? :first : :all), :ad_group_id => self.id )
|
137
228
|
end
|
138
229
|
|
139
|
-
# Converts ad group data to hash - of the same structure which is used when
|
140
|
-
# creating an ad group.
|
141
|
-
#
|
142
|
-
def to_hash
|
143
|
-
ad_group_hash = {
|
144
|
-
:id => self[:id],
|
145
|
-
:name => self[:name],
|
146
|
-
:status => self[:status]
|
147
|
-
}
|
148
|
-
end
|
149
|
-
|
150
230
|
end
|
151
231
|
end
|