Class: Adapi::Campaign

Inherits:
Api
  • Object
show all
Defined in:
lib/adapi/campaign.rb

Constant Summary collapse

NETWORK_SETTING_KEYS =
[ :target_google_search, :target_search_network, 
:target_content_network, :target_partner_search_network ]
ATTRIBUTES =
[ :name, :status, :serving_status, :start_date, :end_date, :budget,
:bidding_strategy, :network_setting, :campaign_stats, :criteria, :ad_groups,
:ad_serving_optimization_status, :settings ]

Constants inherited from Api

Api::API_EXCEPTIONS, Api::LOGGER

Instance Attribute Summary

Attributes inherited from Api

#adwords, #id, #params, #service, #status, #version, #xsi_type

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Api

#[], #[]=, #check_for_errors, create, #mutate, #new?, #persisted?, #store_errors, to_micro_units, #to_param, update

Constructor Details

#initialize(params = {}) ⇒ Campaign

Returns a new instance of Campaign.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/adapi/campaign.rb', line 28

def initialize(params = {})
  params.symbolize_keys!

  params[:service_name] = :CampaignService
  
  @xsi_type = 'Campaign'

  ATTRIBUTES.each do |param_name|
    self.send("#{param_name}=", params[param_name])
  end

  # HOTFIX backward compatibility with old field for criteria
  @criteria ||= params[:targets] || {}

  @ad_groups ||= []

  super(params)
end

Class Method Details

.find(amount = :all, params = {}) ⇒ Object

Searches for campaign/s according to given parameters

Input parameters are dynamic. Special case: single number or string on input is considered to be id and we want to search for a single campaign by id



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/adapi/campaign.rb', line 340

def self.find(amount = :all, params = {})
  # find single campaign by id
  if params.empty? and not amount.is_a?(Symbol)
    params[:id] = amount.to_i
    amount = :first
  end

  params.symbolize_keys!
  first_only = (amount.to_sym == :first)

  predicates = [ :id ].map do |param_name|
    if params[param_name]
      # convert to array
      value = Array.try_convert(params[param_name]) ? params_param_name : [params[param_name]]
      { field: param_name.to_s.camelcase, operator: 'IN', values: value }
    end
  end.compact

  # TODO make configurable (but for the moment, return everything)
  select_fields = %w{ Id Name Status ServingStatus 
    StartDate EndDate AdServingOptimizationStatus } 
  # retrieve CampaignStats fields
  select_fields += %w{ Clicks Impressions Cost Ctr }
  # retrieve Budget fields
  select_fields += %w{ Amount Period DeliveryMethod } 
  # retrieve BiddingStrategy fields
  select_fields += %w{ BiddingStrategy BidCeiling EnhancedCpcEnabled }
  # retrieve NetworkSetting fields
  select_fields += NETWORK_SETTING_KEYS.map { |k| k.to_s.camelize } 

  selector = {
    :fields => select_fields,
    :ordering => [ { field: 'Name', sort_order: 'ASCENDING' } ],
    :predicates => predicates
  }

  response = Campaign.new.service.get(selector)

  response = (response and response[:entries]) ? response[:entries] : []

  response.map! do |campaign_data|
    campaign = Campaign.new(campaign_data)
    # TODO allow mass assignment of :id
    campaign.id = campaign_data[:id]
    campaign
  end

  first_only ? response.first : response
end

.find_complete(campaign_id) ⇒ Object

Returns complete campaign data: criteria, ad groups, keywords and ads. Basically everything what you can set when creating a campaign.



397
398
399
400
401
402
403
404
405
# File 'lib/adapi/campaign.rb', line 397

def self.find_complete(campaign_id)
  campaign = self.find(campaign_id)
  
  campaign[:criteria] = CampaignCriterion.find(:campaign_id => campaign.to_param)

  campaign[:ad_groups] = AdGroup.find(:all, :campaign_id => campaign.to_param).map { |ag| ag.to_hash }

  campaign
end

Instance Method Details

#activateObject



300
# File 'lib/adapi/campaign.rb', line 300

def activate; update(:status => 'ACTIVE'); end

#attributesObject Also known as: to_hash



19
20
21
# File 'lib/adapi/campaign.rb', line 19

def attributes
  super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
end

#bidding_strategy=(params = {}) ⇒ Object

setter for converting bidding_strategy to google format can be either string (just xsi_type) or hash (xsi_type with params) TODO validations for xsi_type

TODO watch out when doing update. according to documentation: “to modify an existing campaign’s bidding strategy, use CampaignOperation.biddingTransition”



71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/adapi/campaign.rb', line 71

def bidding_strategy=(params = {})
  unless params.is_a?(Hash)
    params = { xsi_type: params }
  else
    if params[:bid_ceiling] and not params[:bid_ceiling].is_a?(Hash)
      params[:bid_ceiling] = {
        micro_amount: Api.to_micro_units(params[:bid_ceiling])
      }
    end
  end

  @bidding_strategy = params
end

#budget=(params = {}) ⇒ Object

setter for converting budget to GoogleApi budget can be integer (amount) or hash

TODO return error for missing :amount



90
91
92
93
94
95
96
97
98
99
# File 'lib/adapi/campaign.rb', line 90

def budget=(params = {})
  # if it's single value, it's a budget amount
  params = { amount: params } unless params.is_a?(Hash)

  if params[:amount] and not params[:amount].is_a?(Hash)
    params[:amount] = { micro_amount: Api.to_micro_units(params[:amount]) }
  end

  @budget = params.clone
end

#createObject

create campaign with ad_groups and ads



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/adapi/campaign.rb', line 119

def create
  return false unless self.valid?      
  
  # set defaults for budget for campaign.create only
  self.budget = budget.reverse_merge( period: 'DAILY', delivery_method: 'STANDARD' )

  # create basic campaign attributes
  operand = Hash[
    [ :name, :status, :start_date, :end_date,
      :budget, :bidding_strategy, :network_setting, :settings ].map do |k|
      [ k.to_sym, self.send(k) ] if self.send(k)
    end.compact
  ]

  # set default values for settings (for create only - should we set it also for update?)
  # PS: KeywordMatchSetting is required since 201206
  operand[:settings] ||= []
  unless operand[:settings].map { |s| s[:xsi_type] }.include?('KeywordMatchSetting')
    operand[:settings] << { :xsi_type => 'KeywordMatchSetting', :opt_in => false }
  end

  response = self.mutate( 
    operator: 'ADD', 
    operand: operand
  )

  check_for_errors(self)

  self.id = response[:value].first[:id] rescue nil
  
  if criteria && criteria.size > 0
    new_criteria = Adapi::CampaignCriterion.create(
      campaign_id: @id,
      criteria: criteria
    )

    check_for_errors(new_criteria)
  end

  ad_groups.each do |ad_group_data|
    ad_group = Adapi::AdGroup.create(
      ad_group_data.merge( campaign_id: @id )
    )

    check_for_errors(ad_group, :prefix => "AdGroup \"#{ad_group[:id] || ad_group[:name]}\"")
  end

  self.errors.empty?

rescue CampaignError => e
  false
end

#deleteObject

Deletes campaign - which means simply setting its status to deleted



306
# File 'lib/adapi/campaign.rb', line 306

def delete; update(:status => 'DELETED'); end

#end_date=(a_date) ⇒ Object



51
52
53
# File 'lib/adapi/campaign.rb', line 51

def end_date=(a_date)
  @end_date = parse_date(a_date) if a_date.present?
end

#findObject

Shortcut method, often used for refreshing campaign after create/update REFACTOR into :refresh method



330
331
332
# File 'lib/adapi/campaign.rb', line 330

def find
  Campaign.find(:first, :id => @id)
end

#find_ad_groups(first_only = true) ⇒ Object



390
391
392
# File 'lib/adapi/campaign.rb', line 390

def find_ad_groups(first_only = true)
  AdGroup.find( (first_only ? :first : :all), :campaign_id => self.id )
end

#parse_date(a_date) ⇒ Object



55
56
57
58
59
60
61
# File 'lib/adapi/campaign.rb', line 55

def parse_date(a_date)
  case a_date
    when DateTime, Date, Time then a_date
    # FIXME distiguish between timestamp and YYYYMMDD string
    else DateTime.parse(a_date).strftime('%Y%m%d') 
  end
end

#pauseObject



302
# File 'lib/adapi/campaign.rb', line 302

def pause; update(:status => 'PAUSED'); end

#rename(new_name) ⇒ Object



308
# File 'lib/adapi/campaign.rb', line 308

def rename(new_name); update(:name => new_name); end

#rollback!Object

Deletes campaign if not already deleted. This is usually done after unsuccessfull complex operation (create/update complete campaign)



313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/adapi/campaign.rb', line 313

def rollback!
  if (@status == 'DELETED')
    self.errors.add(:base, 'Campaign is already deleted.')
    return false
  end

  self.errors.clear

  self.update(
    :name => "#{@name}_DELETED_#{(Time.now.to_f * 1000).to_i}",
    :status => 'DELETED'
  )
end

#settings=(setting_options = []) ⇒ Object

setter for campaign settings (array of hashes)



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/adapi/campaign.rb', line 103

def settings=(setting_options = [])
  # for arrays, set in raw form 
  @settings = if setting_options.is_a?(Array)
    setting_options
  # set optional shortcuts for settings
  # :keyword_match_setting => { :opt_in => false } # =>
  # { :xsi_type => 'KeywordMatchSetting', :opt_in => false }
 elsif setting_options.is_a?(Hash)
    setting_options.map do |key, values|
      { :xsi_type => key.to_s.camelcase }.merge(values).symbolize_keys
    end
  end
end

#start_date=(a_date) ⇒ Object



47
48
49
# File 'lib/adapi/campaign.rb', line 47

def start_date=(a_date)
  @start_date = parse_date(a_date) if a_date.present?
end

#update(params = {}) ⇒ Object

Sets campaign data en masse, including criteria and ad_groups with keywords and ads

Warning: campaign data are not refreshed after update! We’d have to do it by get method and that would slow us down. If you want to see latest data, you have to fetch them again manually: Campaign#find or Campaign#find_complete

TODO implement primarily as class method, instance will be just a redirect with campaign_id



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/adapi/campaign.rb', line 180

def update(params = {})
  # REFACTOR for the moment, we use separate campaign object just to prepare and execute 
  # campaign update request. This is kinda ugly and should be eventually refactored (if
  # only because of weird transfer of potential errors later when dealing with response). 
  #
  # campaign basic data workflow: 
  # parse given params by loading them into Campaign.new and reading them back, parsed
  # REFACTOR should be parsed by separate Campaign class method
  #
  campaign = Adapi::Campaign.new(params)
  # HOTFIX remove :service_name param inserted byu initialize method
  params.delete(:service_name)
  # ...and load parsed params back into the hash
  params.keys.each { |k| params[k] = campaign.send(k) }
  params[:id] = @id

  @criteria = params.delete(:criteria)
  params.delete(:targets)
  @ad_groups = params.delete(:ad_groups) || []

  @bidding_strategy = params.delete(:bidding_strategy)

  operation = { 
    operator: 'SET', 
    operand: params
  }

  # BiddingStrategy update has slightly different DSL from other params 
  # https://developers.google.com/adwords/api/docs/reference/v201109_1/CampaignService.BiddingTransition
  #
  # See this post about BiddingTransition limitations:
  # https://groups.google.com/forum/?fromgroups#!topic/adwords-api/tmRk1m7PbhU
  # "ManualCPC can transition to anything and everything else can only transition to ManualCPC" 
  if @bidding_strategy
    operation[:bidding_transition] = { target_bidding_strategy: @bidding_strategy }
  end
 
  campaign.mutate(operation)

  check_for_errors(campaign)

  # update campaign criteria
  if @criteria && @criteria.size > 0
    new_criteria = Adapi::CampaignCriterion.new(
      :campaign_id => @id,
      :criteria => @criteria
    )

    new_criteria.update!

    check_for_errors(new_criteria)        
  end

  self.update_ad_groups!(@ad_groups)

  self.errors.empty?

rescue CampaignError => e
  false
end

#update_ad_groups!(ad_groups = []) ⇒ Object

helper method that updates ad_groups. called from Campaign#update method



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/adapi/campaign.rb', line 243

def update_ad_groups!(ad_groups = [])
  return true if ad_groups.nil? or ad_groups.empty?

  # FIXME deep symbolize_keys
  ad_groups.map! { |ag| ag.symbolize_keys } 

  # check if every ad_group has either :id or :name parameter
  ad_groups.each do |ag|
    if ag[:id].blank? && ag[:name].blank?
      self.errors.add("AdGroup", "required parameter (:id or :name) is missing")
      return false
    end
  end

  # get current ad_groups
  original_ad_groups = AdGroup.find(:all, :campaign_id => @id)

  ad_groups.each do |ad_group_data|
    ad_group_data[:campaign_id] = @id

    # find ad_group by id or name 
    k, v = ad_group_data.has_key?(:id) ? [:id, ad_group_data[:id]] : [:name, ad_group_data[:name]] 
    ad_group = original_ad_groups.find { |ag| ag[k] == v } 

    # update existing ad_group 
    if ad_group.present?
      ad_group.update(ad_group_data)

      original_ad_groups.delete_if { |ag| ag[k] == v }

    # create new ad_group
    # FIXME report error if searching by :id, because such ad_group should exists
    else
      ad_group_data.delete(:id)
      ad_group = AdGroup.create(ad_group_data)
    end

    check_for_errors(ad_group, :prefix => "AdGroup \"#{ad_group[:id] || ad_group[:name]}\"")
  end

  # delete ad_groups which haven't been updated
  original_ad_groups.each do |ag| 
    unless ag.delete
      # FIXME storing error twice for the moment because neither
      # of these errors says all the needed information
      self.errors.add("AdGroup #{ag[:id]}", "could not be deleted")
      self.store_errors(ad_group, "AdGroup #{ag[:id]}")
      return false
    end
  end

  self.errors.empty?

rescue CampaignError => e
  false
end