adapi 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/adapi/api.rb
CHANGED
@@ -1,18 +1,29 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# Basic adapi class, parent of all service classes
|
4
|
+
|
3
5
|
module Adapi
|
4
6
|
class Api
|
5
7
|
extend ActiveModel::Naming
|
6
8
|
include ActiveModel::Validations
|
7
|
-
include ActiveModel::Serialization
|
8
9
|
include ActiveModel::Conversion
|
9
|
-
# TODO include ActiveModel::Dirty
|
10
10
|
|
11
11
|
LOGGER = Config.setup_logger
|
12
12
|
|
13
|
+
API_EXCEPTIONS = [
|
14
|
+
AdsCommon::Errors::ApiException,
|
15
|
+
AdsCommon::Errors::HttpError,
|
16
|
+
AdwordsApi::Errors::ApiException
|
17
|
+
]
|
18
|
+
|
13
19
|
attr_accessor :adwords, :service, :version, :params,
|
14
20
|
:id, :status, :xsi_type
|
15
21
|
|
22
|
+
# Returns hash of attributes for a model instance
|
23
|
+
#
|
24
|
+
# This is an implementation of ActiveRecord::Base#attributes method.
|
25
|
+
# Children of API model customize this method for their own attributes.
|
26
|
+
#
|
16
27
|
def attributes
|
17
28
|
{ 'status' => status, 'xsi_type' => xsi_type }
|
18
29
|
end
|
@@ -48,24 +59,7 @@ module Adapi
|
|
48
59
|
self.send("#{k}=", v)
|
49
60
|
end
|
50
61
|
|
51
|
-
|
52
|
-
# filtered for API calls by default: without :id and :status parameters
|
53
|
-
# PS: attributes method always returns all specified attributes
|
54
|
-
#
|
55
|
-
def data(filtered = true)
|
56
|
-
data_hash = self.serializable_hash.symbolize_keys
|
57
|
-
|
58
|
-
if filtered
|
59
|
-
data_hash.delete(:id)
|
60
|
-
data_hash.delete(:status)
|
61
|
-
end
|
62
|
-
|
63
|
-
data_hash
|
64
|
-
end
|
65
|
-
|
66
|
-
# alias to instance method: data
|
67
|
-
#
|
68
|
-
alias :to_hash :data
|
62
|
+
alias :to_hash :attributes
|
69
63
|
|
70
64
|
# detects whether the instance has been saved already
|
71
65
|
#
|
@@ -74,6 +68,9 @@ module Adapi
|
|
74
68
|
end
|
75
69
|
|
76
70
|
def self.create(params = {})
|
71
|
+
# FIXME deep symbolize_keys, probably through ActiveSupport
|
72
|
+
params.symbolize_keys!
|
73
|
+
|
77
74
|
api_instance = self.new(params)
|
78
75
|
api_instance.create
|
79
76
|
api_instance
|
@@ -84,6 +81,8 @@ module Adapi
|
|
84
81
|
# class
|
85
82
|
#
|
86
83
|
def self.update(params = {})
|
84
|
+
params.symbolize_keys!
|
85
|
+
|
87
86
|
# PS: updating campaign without finding it is much faster
|
88
87
|
api_instance = self.new()
|
89
88
|
api_instance.id = params.delete(:id)
|
@@ -109,15 +108,13 @@ module Adapi
|
|
109
108
|
|
110
109
|
begin
|
111
110
|
response = @service.mutate(operation)
|
112
|
-
|
113
|
-
rescue AdsCommon::Errors::HttpError => e
|
114
|
-
self.errors.add(:base, e.message)
|
115
111
|
|
116
|
-
|
117
|
-
|
118
|
-
error_key =
|
119
|
-
|
120
|
-
|
112
|
+
rescue *API_EXCEPTIONS => e
|
113
|
+
# TODO probably obsolete. keep or remove?
|
114
|
+
# error_key = self.xsi_type.to_s.underscore rescue :base
|
115
|
+
# self.errors.add(error_key, e.message)
|
116
|
+
|
117
|
+
self.errors.add(:base, e.message)
|
121
118
|
end
|
122
119
|
|
123
120
|
response
|
data/lib/adapi/campaign.rb
CHANGED
@@ -1,65 +1,99 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# Class for CampaignService
|
4
|
+
#
|
5
|
+
# https://developers.google.com/adwords/api/docs/reference/latest/CampaignService
|
6
|
+
|
3
7
|
module Adapi
|
4
8
|
class Campaign < Api
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
+
NETWORK_SETTING_KEYS = [ :target_google_search, :target_search_network,
|
11
|
+
:target_content_network, :target_content_contextual, :target_partner_search_network ]
|
12
|
+
|
13
|
+
ATTRIBUTES = [ :name, :status, :serving_status, :start_date, :end_date, :budget,
|
14
|
+
:bidding_strategy, :network_setting, :campaign_stats, :criteria, :ad_groups,
|
15
|
+
:ad_serving_optimization_status ]
|
16
|
+
|
17
|
+
attr_accessor *ATTRIBUTES
|
10
18
|
|
11
19
|
def attributes
|
12
|
-
super.merge
|
13
|
-
'budget' => budget, 'bidding_strategy' => bidding_strategy,
|
14
|
-
'network_setting' => network_setting, 'criteria' => criteria,
|
15
|
-
'ad_groups' => ad_groups)
|
20
|
+
super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
|
16
21
|
end
|
17
22
|
|
23
|
+
alias to_hash attributes
|
24
|
+
|
18
25
|
validates_presence_of :name, :status
|
19
26
|
validates_inclusion_of :status, :in => %w{ ACTIVE DELETED PAUSED }
|
20
27
|
|
21
28
|
def initialize(params = {})
|
29
|
+
params.symbolize_keys!
|
30
|
+
|
22
31
|
params[:service_name] = :CampaignService
|
23
32
|
|
24
33
|
@xsi_type = 'Campaign'
|
25
34
|
|
26
|
-
|
27
|
-
|
28
|
-
self.send "#{param_name}=", params[param_name.to_sym]
|
35
|
+
ATTRIBUTES.each do |param_name|
|
36
|
+
self.send("#{param_name}=", params[param_name])
|
29
37
|
end
|
30
38
|
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
unless @bidding_strategy.is_a?(Hash)
|
36
|
-
@bidding_strategy = { :xsi_type => @bidding_strategy }
|
37
|
-
end
|
39
|
+
# HOTFIX backward compatibility with old field for criteria
|
40
|
+
@criteria ||= params[:targets] || {}
|
41
|
+
|
42
|
+
@ad_groups ||= []
|
38
43
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
super(params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def start_date=(a_date)
|
48
|
+
@start_date = parse_date(a_date) if a_date.present?
|
49
|
+
end
|
50
|
+
|
51
|
+
def end_date=(a_date)
|
52
|
+
@end_date = parse_date(a_date) if a_date.present?
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_date(a_date)
|
56
|
+
case a_date
|
57
|
+
when DateTime, Date, Time then a_date
|
58
|
+
# FIXME distiguish between timestamp and YYYYMMDD string
|
59
|
+
else DateTime.parse(a_date).strftime('%Y%m%d')
|
43
60
|
end
|
61
|
+
end
|
44
62
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
63
|
+
# setter for converting bidding_strategy to google format
|
64
|
+
# can be either string (just xsi_type) or hash (xsi_type with params)
|
65
|
+
# TODO validations for xsi_type
|
66
|
+
#
|
67
|
+
# TODO watch out when doing update. according to documentation:
|
68
|
+
# "to modify an existing campaign's bidding strategy, use
|
69
|
+
# CampaignOperation.biddingTransition"
|
70
|
+
#
|
71
|
+
def bidding_strategy=(params = {})
|
72
|
+
unless params.is_a?(Hash)
|
73
|
+
params = { xsi_type: params }
|
74
|
+
else
|
75
|
+
if params[:bid_ceiling] and not params[:bid_ceiling].is_a?(Hash)
|
76
|
+
params[:bid_ceiling] = {
|
77
|
+
micro_amount: Api.to_micro_units(params[:bid_ceiling])
|
78
|
+
}
|
79
|
+
end
|
53
80
|
end
|
54
|
-
# PS: not sure if this should be a default. maybe we don't even need it
|
55
|
-
@budget[:delivery_method] ||= 'STANDARD'
|
56
81
|
|
57
|
-
|
58
|
-
|
82
|
+
@bidding_strategy = params
|
83
|
+
end
|
59
84
|
|
60
|
-
|
85
|
+
# setter for converting budget to GoogleApi
|
86
|
+
# budget can be integer (amount) or hash
|
87
|
+
#
|
88
|
+
def budget=(params = {})
|
89
|
+
# if it's single value, it's a budget amount
|
90
|
+
params = { amount: params } unless params.is_a?(Hash)
|
61
91
|
|
62
|
-
|
92
|
+
if params[:amount] and not params[:amount].is_a?(Hash)
|
93
|
+
params[:amount] = { micro_amount: Api.to_micro_units(params[:amount]) }
|
94
|
+
end
|
95
|
+
|
96
|
+
@budget = params.merge( period: 'DAILY', delivery_method: 'STANDARD' )
|
63
97
|
end
|
64
98
|
|
65
99
|
# create campaign with ad_groups and ads
|
@@ -109,29 +143,150 @@ module Adapi
|
|
109
143
|
end
|
110
144
|
end
|
111
145
|
|
112
|
-
|
146
|
+
true
|
113
147
|
end
|
114
148
|
|
115
|
-
#
|
116
|
-
#
|
149
|
+
# Sets campaign data en masse, including criteria and ad_groups with keywords and ads
|
150
|
+
#
|
151
|
+
# Warning: campaign data are not refreshed after update! We'd have to do it by get method
|
152
|
+
# and that would slow us down. If you want to see latest data, you have to fetch them again
|
153
|
+
# manually: Campaign#find or Campaign#find_complete
|
117
154
|
#
|
118
|
-
# TODO implement class method
|
155
|
+
# TODO implement primarily as class method, instance will be just a redirect with campaign_id
|
119
156
|
#
|
120
157
|
def update(params = {})
|
121
|
-
#
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
158
|
+
# REFACTOR for the moment, we use separate campaign object just to prepare and execute
|
159
|
+
# campaign update request. This is kinda ugly and should be eventually refactored (if
|
160
|
+
# only because of weird transfer of potential errors later when dealing with response).
|
161
|
+
#
|
162
|
+
# campaign basic data workflow:
|
163
|
+
# parse given params by loading them into Campaign.new and reading them back, parsed
|
164
|
+
# REFACTOR should be parsed by separate Campaign class method
|
165
|
+
#
|
166
|
+
campaign = Adapi::Campaign.new(params)
|
167
|
+
# HOTFIX remove :service_name param inserted byu initialize method
|
168
|
+
params.delete(:service_name)
|
169
|
+
# ...and load parsed params back into the hash
|
170
|
+
params.keys.each { |k| params[k] = campaign.send(k) }
|
171
|
+
params[:id] = @id
|
126
172
|
|
127
|
-
|
173
|
+
@criteria = params.delete(:criteria)
|
174
|
+
params.delete(:targets)
|
175
|
+
@ad_groups = params.delete(:ad_groups) || []
|
176
|
+
|
177
|
+
@bidding_strategy = params.delete(:bidding_strategy)
|
128
178
|
|
129
|
-
|
130
|
-
|
179
|
+
operation = { operator: 'SET', operand: params }
|
180
|
+
|
181
|
+
# BiddingStrategy update has slightly different DSL from other params
|
182
|
+
# https://developers.google.com/adwords/api/docs/reference/v201109_1/CampaignService.BiddingTransition
|
183
|
+
#
|
184
|
+
# See this post about BiddingTransition limitations:
|
185
|
+
# https://groups.google.com/forum/?fromgroups#!topic/adwords-api/tmRk1m7PbhU
|
186
|
+
# "ManualCPC can transition to anything and everything else can only transition to ManualCPC"
|
187
|
+
if @bidding_strategy
|
188
|
+
operation[:bidding_transition] = { target_bidding_strategy: @bidding_strategy }
|
189
|
+
end
|
190
|
+
|
191
|
+
campaign.mutate(operation)
|
192
|
+
|
193
|
+
unless campaign.errors.empty?
|
194
|
+
self.store_errors(campaign) and return false
|
195
|
+
end
|
196
|
+
|
197
|
+
# update campaign criteria
|
198
|
+
if @criteria && @criteria.size > 0
|
199
|
+
new_criteria = Adapi::CampaignCriterion.new(
|
200
|
+
:campaign_id => @id,
|
201
|
+
:criteria => @criteria
|
202
|
+
)
|
203
|
+
|
204
|
+
new_criteria.update!
|
205
|
+
|
206
|
+
unless new_criteria.errors.empty?
|
207
|
+
self.store_errors(new_criteria) and return false
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
result = self.update_ad_groups!(@ad_groups)
|
212
|
+
|
213
|
+
result
|
214
|
+
end
|
215
|
+
|
216
|
+
# helper method that updates ad_groups. called from Campaign#update method
|
217
|
+
#
|
218
|
+
def update_ad_groups!(ad_groups = [])
|
219
|
+
return true if ad_groups.nil? or ad_groups.empty?
|
220
|
+
|
221
|
+
# FIXME deep symbolize_keys
|
222
|
+
ad_groups.map! { |ag| ag.symbolize_keys }
|
223
|
+
|
224
|
+
# check if every ad_group has either :id or :name parameter
|
225
|
+
ad_groups.each do |ag|
|
226
|
+
if ag[:id].blank? && ag[:name].blank?
|
227
|
+
self.errors.add("AdGroup", "required parameter (:id or :name) is missing")
|
228
|
+
return false
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# get current ad_groups
|
233
|
+
original_ad_groups = AdGroup.find(:all, :campaign_id => @id)
|
234
|
+
|
235
|
+
ad_groups.each do |ad_group_data|
|
236
|
+
ad_group_data[:campaign_id] = @id
|
237
|
+
|
238
|
+
# try to find campaign ad_group by id or name
|
239
|
+
k, v = ad_group_data.has_key?(:id) ? [:id, ad_group_data[:id]] : [:name, ad_group_data[:name]]
|
240
|
+
ad_group = original_ad_groups.find { |ag| ag[k] == v }
|
241
|
+
|
242
|
+
# update existing ad_group
|
243
|
+
if ad_group.present?
|
244
|
+
ad_group.update(ad_group_data)
|
245
|
+
|
246
|
+
original_ad_groups.delete_if { |ag| ag[k] == v }
|
247
|
+
|
248
|
+
# create non-existent ad_group
|
249
|
+
# TODO report error if searching by :id, because such ad_group should exists?
|
250
|
+
else
|
251
|
+
ad_group_data.delete(:id)
|
252
|
+
ad_group = AdGroup.create(ad_group_data)
|
253
|
+
end
|
254
|
+
|
255
|
+
unless ad_group.errors.empty?
|
256
|
+
self.store_errors(ad_group, "AdGroup \"#{ad_group[:id] || ad_group[:name]}\"") and return false
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# delete ad_groups which haven't been updated
|
261
|
+
original_ad_groups.each do |ag|
|
262
|
+
unless ag.delete
|
263
|
+
self.errors.add("AdGroup #{ag[:id]}", "could not be deleted")
|
264
|
+
self.store_errors(ad_group, "AdGroup #{ag[:id]}")
|
265
|
+
return false
|
266
|
+
end
|
267
|
+
end
|
131
268
|
|
132
269
|
true
|
133
270
|
end
|
134
271
|
|
272
|
+
# Shortcut for pattern used in Campaign#update method
|
273
|
+
# When partial update fails, store errors in main campaign instance
|
274
|
+
#
|
275
|
+
def store_errors(failed_instance, error_prefix = nil)
|
276
|
+
raise "Campaign#store_errors: Invalid object instance" unless failed_instance.respond_to?(:errors)
|
277
|
+
|
278
|
+
error_prefix ||= failed_instance.respond_to?(:xsi_type) ? failed_instance.xsi_type : nil
|
279
|
+
|
280
|
+
failed_instance.errors.messages.each_pair do |k, v|
|
281
|
+
k = "#{error_prefix}::#{k}" if error_prefix and (k != :base)
|
282
|
+
|
283
|
+
Array(v).each do |x|
|
284
|
+
self.errors.add(k, x)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
|
135
290
|
def activate; update(:status => 'ACTIVE'); end
|
136
291
|
def pause; update(:status => 'PAUSED'); end
|
137
292
|
|
@@ -163,7 +318,6 @@ module Adapi
|
|
163
318
|
Campaign.find(:first, :id => @id)
|
164
319
|
end
|
165
320
|
|
166
|
-
|
167
321
|
# if nothing else than single number or string at the input, assume it's an
|
168
322
|
# id and we want to find campaign by id
|
169
323
|
#
|
@@ -185,10 +339,20 @@ module Adapi
|
|
185
339
|
end
|
186
340
|
end.compact
|
187
341
|
|
188
|
-
# TODO
|
189
|
-
|
342
|
+
# TODO make configurable (but for the moment, return everything)
|
343
|
+
select_fields = %w{ Id Name Status ServingStatus
|
344
|
+
StartDate EndDate AdServingOptimizationStatus }
|
345
|
+
# retrieve CampaignStats fields
|
346
|
+
select_fields += %w{ Clicks Impressions Cost Ctr }
|
347
|
+
# retrieve Budget fields
|
348
|
+
select_fields += %w{ Amount Period DeliveryMethod }
|
349
|
+
# retrieve BiddingStrategy fields
|
350
|
+
select_fields += %w{ BiddingStrategy BidCeiling EnhancedCpcEnabled }
|
351
|
+
# retrieve NetworkSetting fields
|
352
|
+
select_fields += NETWORK_SETTING_KEYS.map { |k| k.to_s.camelize }
|
353
|
+
|
190
354
|
selector = {
|
191
|
-
:fields =>
|
355
|
+
:fields => select_fields,
|
192
356
|
:ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
|
193
357
|
:predicates => predicates
|
194
358
|
}
|
@@ -224,23 +388,5 @@ module Adapi
|
|
224
388
|
campaign
|
225
389
|
end
|
226
390
|
|
227
|
-
# Converts campaign data to hash - of the same structure which is used when
|
228
|
-
# creating a campaign.
|
229
|
-
#
|
230
|
-
# PS: could be implemented more succintly, but let's leave it like this for
|
231
|
-
# now, code can change and this is more readable
|
232
|
-
#
|
233
|
-
def to_hash
|
234
|
-
{
|
235
|
-
:id => self[:id],
|
236
|
-
:name => self[:name],
|
237
|
-
:status => self[:status],
|
238
|
-
:budget => self[:budget],
|
239
|
-
:bidding_strategy => self[:bidding_strategy],
|
240
|
-
:criteria => self[:criteria],
|
241
|
-
:ad_groups => self[:ad_groups]
|
242
|
-
}
|
243
|
-
end
|
244
|
-
|
245
391
|
end
|
246
392
|
end
|
@@ -39,7 +39,7 @@ module Adapi
|
|
39
39
|
super(params)
|
40
40
|
end
|
41
41
|
|
42
|
-
def create
|
42
|
+
def create(operator = 'ADD')
|
43
43
|
# step 1 - convert input hash to new array of criteria
|
44
44
|
# example: :language => [ :en, :cs ] -> [ [:language, :en], [:language, :cs] ]
|
45
45
|
criteria_array = []
|
@@ -124,7 +124,7 @@ module Adapi
|
|
124
124
|
# step 2 - convert individual criteria to low-level google params
|
125
125
|
operations = criteria_array.map do |criterion_type, criterion_settings|
|
126
126
|
{
|
127
|
-
:operator =>
|
127
|
+
:operator => operator,
|
128
128
|
:operand => {
|
129
129
|
:campaign_id => @campaign_id,
|
130
130
|
:criterion => CampaignCriterion::create_criterion(criterion_type, criterion_settings)
|
@@ -136,7 +136,48 @@ module Adapi
|
|
136
136
|
|
137
137
|
(response and response[:value]) ? true : false
|
138
138
|
end
|
139
|
+
|
140
|
+
# custom update method, which delete all current criteria and adds new ones
|
141
|
+
#
|
142
|
+
def update!
|
143
|
+
result = self.delete_all!
|
144
|
+
|
145
|
+
# TODO return error if result == false
|
146
|
+
|
147
|
+
self.create
|
148
|
+
end
|
149
|
+
|
150
|
+
# REFACTOR
|
151
|
+
def destroy
|
152
|
+
self.create('REMOVE')
|
153
|
+
end
|
139
154
|
|
155
|
+
# Deletes all current campaign criteria
|
156
|
+
#
|
157
|
+
def delete_all!
|
158
|
+
# find all current criteria and extract operand params from them
|
159
|
+
original_criteria = CampaignCriterion.find(:campaign_id => @campaign_id).map do |criterion|
|
160
|
+
criterion.select { |k,v| [ :xsi_type, :id ].include?(k) }
|
161
|
+
end
|
162
|
+
|
163
|
+
# HOTFIX temporarily remove platforms, adwords api throws error on no platforms
|
164
|
+
original_criteria.delete_if { |c| c[:xsi_type] == "Platform" }
|
165
|
+
|
166
|
+
operations = original_criteria.map do |criterion|
|
167
|
+
{
|
168
|
+
:operator => 'REMOVE',
|
169
|
+
:operand => {
|
170
|
+
:campaign_id => @campaign_id,
|
171
|
+
:criterion => criterion
|
172
|
+
}
|
173
|
+
}
|
174
|
+
end
|
175
|
+
|
176
|
+
response = self.mutate(operations)
|
177
|
+
|
178
|
+
(response and response[:value]) ? true : false
|
179
|
+
end
|
180
|
+
|
140
181
|
def self.find(params = {})
|
141
182
|
params.symbolize_keys!
|
142
183
|
|
@@ -154,10 +195,8 @@ module Adapi
|
|
154
195
|
end
|
155
196
|
end.compact
|
156
197
|
|
157
|
-
# TODO
|
158
|
-
#
|
198
|
+
# TODO list all applicable fields in select fields
|
159
199
|
selector = {
|
160
|
-
# HOTFIX added LocationName - and values for other criterion types mysteriously appeared as well!
|
161
200
|
:fields => ['Id', 'CriteriaType', 'KeywordText', 'LocationName'],
|
162
201
|
:ordering => [{:field => 'Id', :sort_order => 'ASCENDING'}],
|
163
202
|
:predicates => predicates
|
data/lib/adapi/config.rb
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
# This class
|
4
|
-
|
5
|
-
# TODO enable this way of using configuration
|
6
|
-
# Adapi::Campaign.create(:data => campaign_data, :account => :my_account_alias)
|
3
|
+
# This class holds configuration data for AdWords API
|
7
4
|
|
8
5
|
module Adapi
|
9
6
|
class Config
|
@@ -1,5 +1,15 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# ConstantData::Language provides searching language id by its code. It is used
|
4
|
+
# a a helper function for targeting.
|
5
|
+
#
|
6
|
+
# PS: Previously it has been possible to target by language code. This is not
|
7
|
+
# supported by AdWords API anymore.
|
8
|
+
|
9
|
+
# TODO refactor in tests and targeting code as well, self.find should return nil
|
10
|
+
# if nothing is found
|
11
|
+
# TODO maybe refactor through Array#assoc method
|
12
|
+
|
3
13
|
module Adapi
|
4
14
|
class ConstantData::Language < ConstantData
|
5
15
|
|
@@ -22,11 +32,10 @@ module Adapi
|
|
22
32
|
super(params)
|
23
33
|
end
|
24
34
|
|
25
|
-
#
|
35
|
+
# Finds Language instance by language code or language id (numeric argument)
|
26
36
|
#
|
27
37
|
def self.find(code)
|
28
38
|
|
29
|
-
# let's also allow searching by id
|
30
39
|
if code.is_a?(Integer)
|
31
40
|
Language.new(
|
32
41
|
:id => code,
|
data/lib/adapi/keyword.rb
CHANGED
@@ -84,13 +84,72 @@ module Adapi
|
|
84
84
|
|
85
85
|
response = self.mutate(operations)
|
86
86
|
|
87
|
-
|
87
|
+
# check for PolicyViolationErrors, set exemptions and try again
|
88
|
+
# TODO for now, this is only done once. how about setting a number of retries?
|
89
|
+
unless self.errors[:PolicyViolationError].empty?
|
90
|
+
# FIXME this works, but add exemptions_requests only for related keyword if possible
|
91
|
+
operations.each_with_index do |operation, i|
|
92
|
+
operations[i][:exemption_requests] = self.errors[:PolicyViolationError].map do |error_key|
|
93
|
+
{ :key => error_key }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
self.errors.clear
|
98
|
+
|
99
|
+
response = self.mutate(operations)
|
100
|
+
end
|
101
|
+
|
102
|
+
return false unless self.errors.empty?
|
88
103
|
|
89
104
|
self.keywords = response[:value].map { |keyword| keyword[:criterion] }
|
90
105
|
|
91
106
|
true
|
92
107
|
end
|
93
108
|
|
109
|
+
# keywords mutate wrapper, deals with PolicyViolations for ads
|
110
|
+
#
|
111
|
+
# REFACTOR use just one mutate method in Api class
|
112
|
+
#
|
113
|
+
def mutate(operation)
|
114
|
+
operation = [operation] unless operation.is_a?(Array)
|
115
|
+
|
116
|
+
# fix to save space during specifyng operations
|
117
|
+
operation = operation.map do |op|
|
118
|
+
op[:operand].delete(:status) if op[:operand][:status].nil?
|
119
|
+
op
|
120
|
+
end
|
121
|
+
|
122
|
+
begin
|
123
|
+
|
124
|
+
response = @service.mutate(operation)
|
125
|
+
|
126
|
+
rescue *API_EXCEPTIONS => e
|
127
|
+
|
128
|
+
# return PolicyViolations in specific format so they can be sent again
|
129
|
+
# see adwords-api gem example for details: handle_policy_violation_error.rb
|
130
|
+
e.errors.each do |error|
|
131
|
+
# error[:xsi_type] seems to be broken, so using also alternative key
|
132
|
+
# also could try: :"@xsi:type" (but api_error_type seems to be more robust)
|
133
|
+
if (error[:xsi_type] == 'PolicyViolationError') || (error[:api_error_type] == 'PolicyViolationError')
|
134
|
+
if error[:is_exemptable]
|
135
|
+
self.errors.add(:PolicyViolationError, error[:key])
|
136
|
+
end
|
137
|
+
|
138
|
+
# return also exemptable errors, operation may fail even with them
|
139
|
+
self.errors.add(:base, "violated %s policy: \"%s\" on \"%s\"" % [
|
140
|
+
error[:is_exemptable] ? 'exemptable' : 'non-exemptable',
|
141
|
+
error[:key][:policy_name],
|
142
|
+
error[:key][:violating_text]
|
143
|
+
])
|
144
|
+
else
|
145
|
+
self.errors.add(:base, e.message)
|
146
|
+
end
|
147
|
+
end # of errors.each
|
148
|
+
end
|
149
|
+
|
150
|
+
response
|
151
|
+
end
|
152
|
+
|
94
153
|
def self.find(amount = :all, params = {})
|
95
154
|
params[:format] ||= :google # default, don't do anything with the data from google
|
96
155
|
|
data/lib/adapi/version.rb
CHANGED
@@ -1,12 +1,21 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module Adapi
|
4
|
-
VERSION = "0.0.
|
4
|
+
VERSION = "0.0.9"
|
5
5
|
|
6
6
|
# CHANGELOG:
|
7
7
|
#
|
8
|
+
# 0.0.9
|
9
|
+
# added Campaign#update method - updates campaign, criteria and ad groups by a single method call
|
10
|
+
# fixed PolicyViolation exemptions for keywords and ads
|
11
|
+
# fixed and refactored error handling for most models
|
12
|
+
# refactored AdWords model attributes structure, simplify the code
|
13
|
+
# update find methods for most models to display all attributes
|
14
|
+
# updated gem dependencies (i.e. google-adwords-api to 0.6.2)
|
15
|
+
# refactor SOAP logging - get rid of monkeypatch
|
16
|
+
#
|
8
17
|
# 0.0.8
|
9
|
-
# updated to AdWords API version
|
18
|
+
# updated to AdWords API version v201109_1
|
10
19
|
# updated gem dependencies
|
11
20
|
# removed obsolete monkeypatches
|
12
21
|
# improved SOAP logging - enable pretty logging and configurable log path
|