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
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
|