adapi 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,10 +6,13 @@ module Adapi
6
6
  #
7
7
  class AdGroupCriterion < Api
8
8
 
9
- attr_accessor :ad_group_id, :criterion_use
9
+ ATTRIBUTES = [ :ad_group_id, :criterion_use ]
10
10
 
11
+ attr_accessor *ATTRIBUTES
12
+
13
+ # FIXME why we don't use :criterion_use parameter here?
11
14
  def attributes
12
- super.merge('ad_group_id' => ad_group_id)
15
+ super.merge( ad_group_id: ad_group_id )
13
16
  end
14
17
 
15
18
  def initialize(params = {})
@@ -17,8 +20,8 @@ module Adapi
17
20
 
18
21
  @xsi_type = 'AdGroupCriterion'
19
22
 
20
- %w{ ad_group_id criterion_use }.each do |param_name|
21
- self.send "#{param_name}=", params[param_name.to_sym]
23
+ ATTRIBUTES.each do |param_name|
24
+ self.send "#{param_name}=", params[param_name]
22
25
  end
23
26
 
24
27
  super(params)
@@ -2,22 +2,22 @@
2
2
 
3
3
  module Adapi
4
4
  class AdParam < Api
5
- attr_accessor :ad_group_id, :criterion_id, :insertion_text, :param_index
5
+
6
+ ATTRIBUTES = [ :ad_group_id, :criterion_id, :insertion_text, :param_index ]
7
+
8
+ attr_accessor *ATTRIBUTES
6
9
 
7
10
  validates_presence_of :ad_group_id, :criterion_id
8
11
 
9
12
  def attributes
10
- super.merge(
11
- 'ad_group_id' => ad_group_id, 'criterion_id' => criterion_id,
12
- 'insertion_text' => insertion_text, 'param_index' => param_index
13
- )
13
+ super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
14
14
  end
15
15
 
16
16
  def initialize(params = {})
17
17
  params[:service_name] = :AdParamService
18
18
 
19
- %w{ ad_group_id criterion_id insertion_text param_index }.each do |param_name|
20
- self.send "#{param_name}=", params[param_name.to_sym]
19
+ ATTRIBUTES.each do |param_name|
20
+ self.send "#{param_name}=", params[param_name]
21
21
  end
22
22
 
23
23
  super(params)
@@ -26,7 +26,7 @@ module Adapi
26
26
  def create
27
27
  operation = {
28
28
  :operator => 'SET',
29
- :operand => serializable_hash
29
+ :operand => self.attributes
30
30
  }
31
31
 
32
32
  begin
@@ -77,13 +77,5 @@ module Adapi
77
77
  response
78
78
  end
79
79
 
80
- def serializable_hash
81
- {
82
- :ad_group_id => ad_group_id,
83
- :criterion_id => criterion_id,
84
- :param_index => param_index,
85
- :insertion_text => insertion_text
86
- }
87
- end
88
80
  end
89
81
  end
@@ -2,6 +2,10 @@
2
2
 
3
3
  # Basic adapi class, parent of all service classes
4
4
 
5
+ # TODO create universal Api.attributes method (instead of having the same method in all subclasses)
6
+ # TODO create universal Api.initialize method (some subclasses don't have to have their own initialize method)
7
+ # TODO move common methods into separate Common class or module
8
+
5
9
  module Adapi
6
10
  class Api
7
11
  extend ActiveModel::Naming
@@ -16,6 +20,13 @@ module Adapi
16
20
  AdwordsApi::Errors::ApiException
17
21
  ]
18
22
 
23
+ # these exceptions help to control program flow
24
+ # during complex operations over campaigns and ad_groups
25
+ #
26
+ class ApiError < Exception; end
27
+ class CampaignError < Exception; end
28
+ class AdGroupError < Exception; end
29
+
19
30
  attr_accessor :adwords, :service, :version, :params,
20
31
  :id, :status, :xsi_type
21
32
 
@@ -25,7 +36,7 @@ module Adapi
25
36
  # Children of API model customize this method for their own attributes.
26
37
  #
27
38
  def attributes
28
- { 'status' => status, 'xsi_type' => xsi_type }
39
+ { status: status, xsi_type: xsi_type }
29
40
  end
30
41
 
31
42
  def initialize(params = {})
@@ -69,7 +80,7 @@ module Adapi
69
80
 
70
81
  def self.create(params = {})
71
82
  # FIXME deep symbolize_keys, probably through ActiveSupport
72
- params.symbolize_keys!
83
+ params.symbolize_keys! if params.is_a?(Hash)
73
84
 
74
85
  api_instance = self.new(params)
75
86
  api_instance.create
@@ -92,7 +103,6 @@ module Adapi
92
103
  api_instance
93
104
  end
94
105
 
95
-
96
106
  # wrap AdWords add/update/destroy actions and deals with errors
97
107
  # PS: Keyword and Ad models have their own wrappers because of
98
108
  # PolicyViolations
@@ -106,20 +116,90 @@ module Adapi
106
116
  op
107
117
  end
108
118
 
109
- begin
119
+ begin
120
+
110
121
  response = @service.mutate(operation)
111
122
 
112
123
  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
124
 
117
- self.errors.add(:base, e.message)
125
+ unless e.respond_to?(:errors)
126
+ self.errors.add(:base, e.message)
127
+ return false
128
+ end
129
+
130
+ e.errors.each do |error|
131
+ if (error[:xsi_type] == 'PolicyViolationError') || (error[:api_error_type] == 'PolicyViolationError')
132
+ # return exemptable PolicyViolations errors in custom format so we can request exemptions
133
+ # see adwords-api gem example for details: handle_policy_violation_error.rb
134
+ # so far, this applies only for keywords and ads
135
+ if error[:is_exemptable]
136
+ self.errors.add( :PolicyViolationError, error[:key].merge(
137
+ :operation_index => AdwordsApi::Utils.operation_index_for_error(error)
138
+ ) )
139
+ end
140
+
141
+ # besides PolicyViolations errors in custom format, return all errors also in regular format
142
+ self.errors.add(:base, "violated %s policy: \"%s\" on \"%s\"" % [
143
+ error[:is_exemptable] ? 'exemptable' : 'non-exemptable',
144
+ error[:key][:policy_name],
145
+ error[:key][:violating_text]
146
+ ])
147
+ else
148
+ self.errors.add(:base, e.message)
149
+ end
150
+ end # of errors.each
151
+
152
+ false
118
153
  end
119
154
 
120
155
  response
121
156
  end
122
157
 
158
+ # Deals with campaign exceptions encountered during complex operations over AdWords API
159
+ #
160
+ # Parameters:
161
+ # store_errors (default: true) - add errors to self.error collection
162
+ # raise_errors (default: false) - raises exception CampaignError (after optional saving errors)
163
+ #
164
+ def check_for_errors(adapi_instance, options = {})
165
+ options.merge!( store_errors: true, raise_errors: false )
166
+
167
+ # don't store errors in this case, because errors are already there
168
+ # and loop in store_errors method would cause application to hang
169
+ options[:store_errors] = false if (adapi_instance == self)
170
+
171
+ unless adapi_instance.errors.empty?
172
+ store_errors(adapi_instance, options[:prefix]) if options[:store_errors]
173
+
174
+ if options[:raise_errors]
175
+ exception_type = case adapi_instance.xsi_type
176
+ when "Campaign" then CampaignError
177
+ when "AdGroup" then AdGroupError
178
+ else ApiError
179
+ end
180
+
181
+ raise exception_type
182
+ end
183
+ end
184
+ end
185
+
186
+ # Shortcut for pattern used in Campaign#update method
187
+ # When partial update fails, store errors in main campaign instance
188
+ #
189
+ def store_errors(failed_instance, error_prefix = nil)
190
+ raise "#{failed_instance.xsi_type}#store_errors: Invalid object instance" unless failed_instance.respond_to?(:errors)
191
+
192
+ error_prefix ||= failed_instance.respond_to?(:xsi_type) ? failed_instance.xsi_type : nil
193
+
194
+ failed_instance.errors.messages.each_pair do |k, v|
195
+ k = "#{error_prefix}::#{k}" if error_prefix and (k != :base)
196
+
197
+ Array(v).each do |x|
198
+ self.errors.add(k, x)
199
+ end
200
+ end
201
+ end
202
+
123
203
  # convert number to micro units (unit * one million)
124
204
  #
125
205
  def self.to_micro_units(x)
@@ -12,7 +12,7 @@ module Adapi
12
12
 
13
13
  ATTRIBUTES = [ :name, :status, :serving_status, :start_date, :end_date, :budget,
14
14
  :bidding_strategy, :network_setting, :campaign_stats, :criteria, :ad_groups,
15
- :ad_serving_optimization_status ]
15
+ :ad_serving_optimization_status, :settings ]
16
16
 
17
17
  attr_accessor *ATTRIBUTES
18
18
 
@@ -96,54 +96,72 @@ module Adapi
96
96
  @budget = params.merge( period: 'DAILY', delivery_method: 'STANDARD' )
97
97
  end
98
98
 
99
+ # setter for campaign settings (array of hashes)
100
+ #
101
+ def settings=(setting_options = [])
102
+ # for arrays, set in raw form
103
+ @settings = if setting_options.is_a?(Array)
104
+ setting_options
105
+ # set optional shortcuts for settings
106
+ # :keyword_match_setting => { :opt_in => false } # =>
107
+ # { :xsi_type => 'KeywordMatchSetting', :opt_in => false }
108
+ elsif setting_options.is_a?(Hash)
109
+ setting_options.map do |key, values|
110
+ { :xsi_type => key.to_s.camelcase }.merge(values).symbolize_keys
111
+ end
112
+ end
113
+ end
114
+
99
115
  # create campaign with ad_groups and ads
100
116
  #
101
117
  def create
102
118
  return false unless self.valid?
103
119
 
120
+ # create basic campaign attributes
104
121
  operand = Hash[
105
122
  [ :name, :status, :start_date, :end_date,
106
- :budget, :bidding_strategy, :network_setting ].map do |k|
123
+ :budget, :bidding_strategy, :network_setting, :settings ].map do |k|
107
124
  [ k.to_sym, self.send(k) ] if self.send(k)
108
125
  end.compact
109
126
  ]
110
127
 
111
- response = self.mutate(
112
- :operator => 'ADD',
113
- :operand => operand
128
+ # set default values for settings (for create only - should we set it also for update?)
129
+ # PS: KeywordMatchSetting is required since 201206
130
+ operand[:settings] ||= []
131
+ unless operand[:settings].map { |s| s[:xsi_type] }.include?('KeywordMatchSetting')
132
+ operand[:settings] << { :xsi_type => 'KeywordMatchSetting', :opt_in => false }
133
+ end
134
+
135
+ response = self.mutate(
136
+ operator: 'ADD',
137
+ operand: operand
114
138
  )
115
-
116
- return false unless (response and response[:value])
117
-
139
+
140
+ check_for_errors(self)
141
+
118
142
  self.id = response[:value].first[:id] rescue nil
119
143
 
120
- # create criteria (former targets) if they are available
121
- if criteria.size > 0
122
- criterion = Adapi::CampaignCriterion.create(
123
- :campaign_id => @id,
124
- :criteria => criteria
144
+ if criteria && criteria.size > 0
145
+ new_criteria = Adapi::CampaignCriterion.create(
146
+ campaign_id: @id,
147
+ criteria: criteria
125
148
  )
126
-
127
- if (criterion.errors.size > 0)
128
- self.errors.add("[campaign criterion]", criterion.errors.to_a)
129
- self.rollback
130
- return false
131
- end
149
+
150
+ check_for_errors(new_criteria)
132
151
  end
133
152
 
134
153
  ad_groups.each do |ad_group_data|
135
154
  ad_group = Adapi::AdGroup.create(
136
- ad_group_data.merge(:campaign_id => @id)
155
+ ad_group_data.merge( campaign_id: @id )
137
156
  )
138
157
 
139
- if (ad_group.errors.size > 0)
140
- self.errors.add("[ad group] \"#{ad_group.name}\"", ad_group.errors.to_a)
141
- self.rollback
142
- return false
143
- end
158
+ check_for_errors(ad_group, :prefix => "AdGroup \"#{ad_group[:id] || ad_group[:name]}\"")
144
159
  end
145
160
 
146
- true
161
+ self.errors.empty?
162
+
163
+ rescue CampaignError => e
164
+ false
147
165
  end
148
166
 
149
167
  # Sets campaign data en masse, including criteria and ad_groups with keywords and ads
@@ -176,7 +194,10 @@ module Adapi
176
194
 
177
195
  @bidding_strategy = params.delete(:bidding_strategy)
178
196
 
179
- operation = { operator: 'SET', operand: params }
197
+ operation = {
198
+ operator: 'SET',
199
+ operand: params
200
+ }
180
201
 
181
202
  # BiddingStrategy update has slightly different DSL from other params
182
203
  # https://developers.google.com/adwords/api/docs/reference/v201109_1/CampaignService.BiddingTransition
@@ -190,9 +211,7 @@ module Adapi
190
211
 
191
212
  campaign.mutate(operation)
192
213
 
193
- unless campaign.errors.empty?
194
- self.store_errors(campaign) and return false
195
- end
214
+ check_for_errors(campaign)
196
215
 
197
216
  # update campaign criteria
198
217
  if @criteria && @criteria.size > 0
@@ -202,15 +221,16 @@ module Adapi
202
221
  )
203
222
 
204
223
  new_criteria.update!
205
-
206
- unless new_criteria.errors.empty?
207
- self.store_errors(new_criteria) and return false
208
- end
224
+
225
+ check_for_errors(new_criteria)
209
226
  end
210
227
 
211
- result = self.update_ad_groups!(@ad_groups)
228
+ self.update_ad_groups!(@ad_groups)
212
229
 
213
- result
230
+ self.errors.empty?
231
+
232
+ rescue CampaignError => e
233
+ false
214
234
  end
215
235
 
216
236
  # helper method that updates ad_groups. called from Campaign#update method
@@ -235,7 +255,7 @@ module Adapi
235
255
  ad_groups.each do |ad_group_data|
236
256
  ad_group_data[:campaign_id] = @id
237
257
 
238
- # try to find campaign ad_group by id or name
258
+ # find ad_group by id or name
239
259
  k, v = ad_group_data.has_key?(:id) ? [:id, ad_group_data[:id]] : [:name, ad_group_data[:name]]
240
260
  ad_group = original_ad_groups.find { |ag| ag[k] == v }
241
261
 
@@ -245,84 +265,75 @@ module Adapi
245
265
 
246
266
  original_ad_groups.delete_if { |ag| ag[k] == v }
247
267
 
248
- # create non-existent ad_group
249
- # TODO report error if searching by :id, because such ad_group should exists?
268
+ # create new ad_group
269
+ # FIXME report error if searching by :id, because such ad_group should exists
250
270
  else
251
271
  ad_group_data.delete(:id)
252
272
  ad_group = AdGroup.create(ad_group_data)
253
273
  end
254
274
 
255
- unless ad_group.errors.empty?
256
- self.store_errors(ad_group, "AdGroup \"#{ad_group[:id] || ad_group[:name]}\"") and return false
257
- end
275
+ check_for_errors(ad_group, :prefix => "AdGroup \"#{ad_group[:id] || ad_group[:name]}\"")
258
276
  end
259
277
 
260
278
  # delete ad_groups which haven't been updated
261
279
  original_ad_groups.each do |ag|
262
280
  unless ag.delete
281
+ # FIXME storing error twice for the moment because neither
282
+ # of these errors says all the needed information
263
283
  self.errors.add("AdGroup #{ag[:id]}", "could not be deleted")
264
284
  self.store_errors(ad_group, "AdGroup #{ag[:id]}")
265
285
  return false
266
286
  end
267
287
  end
268
288
 
269
- true
270
- end
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
289
+ self.errors.empty?
279
290
 
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
291
+ rescue CampaignError => e
292
+ false
287
293
  end
288
294
 
289
-
290
295
  def activate; update(:status => 'ACTIVE'); end
296
+
291
297
  def pause; update(:status => 'PAUSED'); end
292
298
 
293
- # Deletes campaign - which means, sets its status to deleted (because
294
- # AdWords campaigns are never really deleted.)
299
+ # Deletes campaign - which means simply setting its status to deleted
295
300
  #
296
301
  def delete; update(:status => 'DELETED'); end
297
302
 
298
303
  def rename(new_name); update(:name => new_name); end
299
304
 
300
- # when Campaign#create fails, "delete" campaign
301
-
302
- # Deletes campaign if it's not already deleted. For more information about
303
- # "deleted" campaigns, see `delete` method
305
+ # Deletes campaign if not already deleted. This is usually done after
306
+ # unsuccessfull complex operation (create/update complete campaign)
304
307
  #
305
- def rollback
308
+ def rollback!
306
309
  if (@status == 'DELETED')
307
310
  self.errors.add(:base, 'Campaign is already deleted.')
308
311
  return false
309
312
  end
310
313
 
311
- update(
314
+ self.errors.clear
315
+
316
+ self.update(
312
317
  :name => "#{@name}_DELETED_#{(Time.now.to_f * 1000).to_i}",
313
318
  :status => 'DELETED'
314
319
  )
315
320
  end
316
321
 
317
- def find # == refresh
322
+ # Shortcut method, often used for refreshing campaign after create/update
323
+ # REFACTOR into :refresh method
324
+ #
325
+ def find
318
326
  Campaign.find(:first, :id => @id)
319
327
  end
320
328
 
321
- # if nothing else than single number or string at the input, assume it's an
322
- # id and we want to find campaign by id
329
+ # Searches for campaign/s according to given parameters
330
+ #
331
+ # Input parameters are dynamic.
332
+ # Special case: single number or string on input is considered to be id
333
+ # and we want to search for a single campaign by id
323
334
  #
324
335
  def self.find(amount = :all, params = {})
325
- # find campaign by id - related syntactic sugar
336
+ # find single campaign by id
326
337
  if params.empty? and not amount.is_a?(Symbol)
327
338
  params[:id] = amount.to_i
328
339
  amount = :first
@@ -335,7 +346,7 @@ module Adapi
335
346
  if params[param_name]
336
347
  # convert to array
337
348
  value = Array.try_convert(params[param_name]) ? params_param_name : [params[param_name]]
338
- {:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
349
+ { field: param_name.to_s.camelcase, operator: 'IN', values: value }
339
350
  end
340
351
  end.compact
341
352
 
@@ -347,13 +358,13 @@ module Adapi
347
358
  # retrieve Budget fields
348
359
  select_fields += %w{ Amount Period DeliveryMethod }
349
360
  # retrieve BiddingStrategy fields
350
- select_fields += %w{ BiddingStrategy BidCeiling EnhancedCpcEnabled }
361
+ select_fields += %w{ BiddingStrategy BidCeiling EnhancedCpcEnabled }
351
362
  # retrieve NetworkSetting fields
352
363
  select_fields += NETWORK_SETTING_KEYS.map { |k| k.to_s.camelize }
353
364
 
354
365
  selector = {
355
366
  :fields => select_fields,
356
- :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
367
+ :ordering => [ { field: 'Name', sort_order: 'ASCENDING' } ],
357
368
  :predicates => predicates
358
369
  }
359
370
 
@@ -383,7 +394,7 @@ module Adapi
383
394
 
384
395
  campaign[:criteria] = CampaignCriterion.find(:campaign_id => campaign.to_param)
385
396
 
386
- campaign[:ad_groups] = AdGroup.find(:all, :campaign_id => campaign.to_param)
397
+ campaign[:ad_groups] = AdGroup.find(:all, :campaign_id => campaign.to_param).map { |ag| ag.to_hash }
387
398
 
388
399
  campaign
389
400
  end