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.
- data/README.markdown +35 -27
- data/adapi.gemspec +8 -8
- data/examples/add_ad_group.rb +13 -7
- data/examples/add_bare_ad_group.rb +12 -6
- data/examples/add_bare_campaign.rb +13 -6
- data/examples/add_campaign.rb +22 -10
- data/examples/add_campaign_criteria.rb +22 -8
- data/examples/add_invalid_ad_group.rb +9 -5
- data/examples/add_invalid_keywords.rb +3 -0
- data/examples/add_invalid_text_ad.rb +12 -6
- data/examples/add_keywords.rb +20 -9
- data/examples/add_text_ads.rb +45 -0
- data/examples/rollback_campaign.rb +37 -14
- data/examples/update_campaign.rb +1 -5
- data/examples/update_complete_campaign.rb +3 -4
- data/lib/adapi.rb +5 -4
- data/lib/adapi/ad.rb +14 -39
- data/lib/adapi/ad/text_ad.rb +38 -23
- data/lib/adapi/ad_group.rb +35 -30
- data/lib/adapi/ad_group_criterion.rb +7 -4
- data/lib/adapi/ad_param.rb +8 -16
- data/lib/adapi/api.rb +88 -8
- data/lib/adapi/campaign.rb +87 -76
- data/lib/adapi/campaign_criterion.rb +6 -4
- data/lib/adapi/campaign_target.rb +6 -4
- data/lib/adapi/config.rb +2 -2
- data/lib/adapi/keyword.rb +11 -58
- data/lib/adapi/version.rb +7 -1
- data/test/integration/create_ad_group_test.rb +48 -0
- data/test/integration/create_campaign_test.rb +1 -4
- data/test/test_helper.rb +49 -0
- metadata +43 -41
@@ -6,10 +6,13 @@ module Adapi
|
|
6
6
|
#
|
7
7
|
class AdGroupCriterion < Api
|
8
8
|
|
9
|
-
|
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(
|
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
|
-
|
21
|
-
self.send "#{param_name}=", params[param_name
|
23
|
+
ATTRIBUTES.each do |param_name|
|
24
|
+
self.send "#{param_name}=", params[param_name]
|
22
25
|
end
|
23
26
|
|
24
27
|
super(params)
|
data/lib/adapi/ad_param.rb
CHANGED
@@ -2,22 +2,22 @@
|
|
2
2
|
|
3
3
|
module Adapi
|
4
4
|
class AdParam < Api
|
5
|
-
|
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
|
-
|
20
|
-
self.send "#{param_name}=", params[param_name
|
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 =>
|
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
|
data/lib/adapi/api.rb
CHANGED
@@ -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
|
-
{
|
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
|
-
|
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)
|
data/lib/adapi/campaign.rb
CHANGED
@@ -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
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
117
|
-
|
139
|
+
|
140
|
+
check_for_errors(self)
|
141
|
+
|
118
142
|
self.id = response[:value].first[:id] rescue nil
|
119
143
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
:
|
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
|
-
|
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(:
|
155
|
+
ad_group_data.merge( campaign_id: @id )
|
137
156
|
)
|
138
157
|
|
139
|
-
|
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
|
-
|
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 = {
|
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
|
-
|
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
|
-
|
207
|
-
self.store_errors(new_criteria) and return false
|
208
|
-
end
|
224
|
+
|
225
|
+
check_for_errors(new_criteria)
|
209
226
|
end
|
210
227
|
|
211
|
-
|
228
|
+
self.update_ad_groups!(@ad_groups)
|
212
229
|
|
213
|
-
|
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
|
-
#
|
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
|
249
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
281
|
-
|
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
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
#
|
322
|
-
#
|
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
|
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
|
-
{:
|
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
|
-
|
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 => [{:
|
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
|