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.
Files changed (55) hide show
  1. data/.gitignore +1 -1
  2. data/README.markdown +2 -1
  3. data/adapi.gemspec +11 -11
  4. data/examples/add_ad_group.rb +0 -0
  5. data/examples/add_bare_ad_group.rb +3 -1
  6. data/examples/add_bare_campaign.rb +6 -4
  7. data/examples/add_campaign.rb +10 -3
  8. data/examples/add_campaign_criteria.rb +5 -5
  9. data/examples/add_invalid_ad_group.rb +4 -6
  10. data/examples/add_invalid_keywords.rb +40 -0
  11. data/examples/add_invalid_text_ad.rb +6 -6
  12. data/examples/add_keywords.rb +0 -0
  13. data/examples/add_negative_campaign_criteria.rb +0 -0
  14. data/examples/add_text_ad.rb +5 -9
  15. data/examples/custom_settings.yml +8 -0
  16. data/examples/customize_configuration.rb +0 -0
  17. data/examples/delete_ad_group.rb +11 -0
  18. data/examples/delete_keyword.rb +0 -0
  19. data/examples/delete_text_ad.rb +13 -0
  20. data/examples/find_ad_group.rb +10 -0
  21. data/examples/find_all_campaigns.rb +0 -0
  22. data/examples/find_bare_campaign.rb +34 -0
  23. data/examples/find_campaign.rb +0 -0
  24. data/examples/find_campaign_ad_groups.rb +0 -0
  25. data/examples/find_campaign_criteria.rb +0 -0
  26. data/examples/find_locations.rb +0 -0
  27. data/examples/log_to_specific_account.rb +0 -0
  28. data/examples/rollback_campaign.rb +0 -0
  29. data/examples/test_diacritics.rb +0 -0
  30. data/examples/update_ad_group.rb +70 -0
  31. data/examples/update_campaign.rb +34 -9
  32. data/examples/update_campaign_ad_groups.rb +137 -0
  33. data/examples/update_campaign_criteria.rb +33 -0
  34. data/examples/update_complete_campaign.rb +177 -0
  35. data/examples/update_text_ad.rb +18 -0
  36. data/lib/adapi/ad/text_ad.rb +39 -49
  37. data/lib/adapi/ad.rb +30 -19
  38. data/lib/adapi/ad_group.rb +110 -30
  39. data/lib/adapi/api.rb +25 -28
  40. data/lib/adapi/campaign.rb +216 -70
  41. data/lib/adapi/campaign_criterion.rb +44 -5
  42. data/lib/adapi/config.rb +1 -4
  43. data/lib/adapi/constant_data/language.rb +11 -2
  44. data/lib/adapi/keyword.rb +60 -1
  45. data/lib/adapi/version.rb +11 -2
  46. data/lib/adapi.rb +10 -7
  47. data/test/{config/adapi.yml.template → fixtures/adapi.yml} +11 -2
  48. data/test/{config/adwords_api.yml.template → fixtures/adwords_api.yml} +6 -2
  49. data/test/integration/find_location_test.rb +1 -1
  50. data/test/unit/ad_group_test.rb +2 -2
  51. data/test/unit/config_test.rb +8 -27
  52. data/test/unit/constant_data/language_test.rb +34 -0
  53. metadata +52 -108
  54. data/examples/update_campaign_status.rb +0 -15
  55. 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
- # return parameters in hash
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
- # traps any exceptions raised by AdWords API
117
- rescue AdwordsApi::Errors::ApiException => e
118
- error_key = "[#{self.xsi_type.underscore}]"
119
-
120
- self.errors.add(error_key, e.message)
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
@@ -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
- # http://code.google.com/apis/adwords/docs/reference/latest/CampaignService.Campaign.html
7
- #
8
- attr_accessor :name, :serving_status, :start_date, :end_date, :budget,
9
- :bidding_strategy, :network_setting, :criteria, :ad_groups
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('name' => name, 'start_date' => start_date, 'end_date' => end_date,
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
- %w{ name status start_date end_date budget bidding_strategy
27
- network_setting criteria ad_groups}.each do |param_name|
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
- # convert bidding_strategy to GoogleApi
32
- # can be either string (just xsi_type) or hash (xsi_type with params)
33
- # TODO validations for xsi_type
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
- if @bidding_strategy[:bid_ceiling] and not @bidding_strategy[:bid_ceiling].is_a?(Hash)
40
- @bidding_strategy[:bid_ceiling] = {
41
- :micro_amount => Api.to_micro_units(@bidding_strategy[:bid_ceiling])
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
- # convert budget to GoogleApi
46
- # TODO validations for budget
47
- #
48
- # budget can be integer (amount) or hash
49
- @budget = { :amount => @budget } unless @budget.is_a?(Hash)
50
- @budget[:period] ||= 'DAILY'
51
- if @budget[:amount] and not @budget[:amount].is_a?(Hash)
52
- @budget[:amount] = { :micro_amount => Api.to_micro_units(@budget[:amount]) }
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
- # HOTFIX backward compatibility with old field for criteria
58
- @criteria ||= params[:targets] || {}
82
+ @bidding_strategy = params
83
+ end
59
84
 
60
- @ad_groups ||= []
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
- super(params)
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
- return true
146
+ true
113
147
  end
114
148
 
115
- # general method for changing campaign data
116
- # TODO enable updating of all campaign parts at once, same as for Campaign#create method
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
- # HOTFIX can't use current instance, gotta create new one
122
- response = Adapi::Campaign.new().mutate(
123
- :operator => 'SET',
124
- :operand => params.merge(:id => @id)
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
- return false unless (response and response[:value])
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
- # faster than self.find
130
- params.each_pair { |k,v| self.send("#{k}=", v) }
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 display the rest of the data
189
- # TODO get NetworkSetting - setting as in fields doesn't work
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 => ['Id', 'Name', 'Status', 'BiddingStrategy'],
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 => 'ADD',
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: get more fields - tricky, because value files differ for most types
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 hold configuration data for AdWords API
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
- # Returns AdWords API language id based for language code
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
- return false unless (response and response[:value])
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.8"
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 v201109
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