adapi 0.0.9 → 0.1.0

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