adapi 0.0.4 → 0.0.5

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 (49) hide show
  1. data/.gitignore +3 -0
  2. data/GUIDELINES.markdown +61 -0
  3. data/README.markdown +79 -45
  4. data/Rakefile +31 -3
  5. data/adapi.gemspec +10 -3
  6. data/examples/add_ad_group.rb +1 -1
  7. data/examples/add_bare_ad_group.rb +1 -4
  8. data/examples/add_campaign.rb +1 -0
  9. data/examples/add_campaign_criteria.rb +27 -0
  10. data/examples/add_invalid_ad_group.rb +3 -1
  11. data/examples/add_invalid_text_ad.rb +1 -1
  12. data/examples/add_keywords.rb +1 -1
  13. data/examples/add_negative_campaign_criteria.rb +23 -0
  14. data/examples/add_text_ad.rb +1 -1
  15. data/examples/customize_configuration.rb +1 -2
  16. data/examples/delete_keyword.rb +1 -1
  17. data/examples/find_campaign.rb +5 -5
  18. data/examples/find_campaign_ad_groups.rb +1 -1
  19. data/examples/find_campaign_criteria.rb +103 -0
  20. data/examples/find_locations.rb +21 -0
  21. data/examples/rollback_campaign.rb +7 -6
  22. data/examples/update_campaign.rb +1 -1
  23. data/examples/update_campaign_status.rb +1 -1
  24. data/lib/adapi.rb +11 -9
  25. data/lib/adapi/ad/text_ad.rb +2 -1
  26. data/lib/adapi/ad_group.rb +13 -9
  27. data/lib/adapi/ad_param.rb +89 -0
  28. data/lib/adapi/api.rb +8 -0
  29. data/lib/adapi/campaign.rb +27 -18
  30. data/lib/adapi/campaign_criterion.rb +278 -0
  31. data/lib/adapi/campaign_target.rb +5 -123
  32. data/lib/adapi/config.rb +36 -31
  33. data/lib/adapi/constant_data.rb +13 -0
  34. data/lib/adapi/constant_data/language.rb +45 -0
  35. data/lib/adapi/keyword.rb +15 -5
  36. data/lib/adapi/location.rb +91 -0
  37. data/lib/adapi/version.rb +8 -1
  38. data/lib/httpi_request_monkeypatch.rb +4 -0
  39. data/test/config/adapi.yml.template +21 -0
  40. data/test/config/adwords_api.yml.template +10 -0
  41. data/test/integration/create_campaign_test.rb +54 -0
  42. data/test/test_helper.rb +2 -3
  43. data/test/unit/ad_group_test.rb +3 -4
  44. data/test/unit/ad_test.rb +1 -1
  45. data/test/unit/campaign_criterion_test.rb +23 -0
  46. data/test/unit/config_test.rb +52 -0
  47. metadata +48 -35
  48. data/examples/add_campaign_targets.rb +0 -26
  49. data/test/unit/campaign_target_test.rb +0 -51
data/lib/adapi/api.rb CHANGED
@@ -28,6 +28,14 @@ module Adapi
28
28
  @version = API_VERSION
29
29
  @service = @adwords.service(params[:service_name].to_sym, @version)
30
30
  @params = params
31
+
32
+ # TODO add switched for logging (true/false) and log location
33
+ log_level = Adapi::Config.read[:library][:log_level] rescue nil
34
+ if log_level
35
+ logger = Logger.new( File.join(ENV['HOME'], (params[:logfile] || 'adapi.log')) )
36
+ logger.level = eval("Logger::%s" % Adapi::Config.read[:library][:log_level].to_s.upcase)
37
+ @adwords.logger = logger
38
+ end
31
39
  end
32
40
 
33
41
  def to_param
@@ -6,12 +6,12 @@ module Adapi
6
6
  # http://code.google.com/apis/adwords/docs/reference/latest/CampaignService.Campaign.html
7
7
  #
8
8
  attr_accessor :name, :serving_status, :start_date, :end_date, :budget,
9
- :bidding_strategy, :network_setting, :targets, :ad_groups
9
+ :bidding_strategy, :network_setting, :criteria, :ad_groups
10
10
 
11
11
  def attributes
12
12
  super.merge('name' => name, 'start_date' => start_date, 'end_date' => end_date,
13
13
  'budget' => budget, 'bidding_strategy' => bidding_strategy,
14
- 'network_setting' => network_setting, 'targets' => targets,
14
+ 'network_setting' => network_setting, 'criteria' => criteria,
15
15
  'ad_groups' => ad_groups)
16
16
  end
17
17
 
@@ -24,7 +24,7 @@ module Adapi
24
24
  @xsi_type = 'Campaign'
25
25
 
26
26
  %w{ name status start_date end_date budget bidding_strategy
27
- network_setting targets ad_groups}.each do |param_name|
27
+ network_setting criteria ad_groups}.each do |param_name|
28
28
  self.send "#{param_name}=", params[param_name.to_sym]
29
29
  end
30
30
 
@@ -54,7 +54,7 @@ module Adapi
54
54
  # PS: not sure if this should be a default. maybe we don't even need it
55
55
  @budget[:delivery_method] ||= 'STANDARD'
56
56
 
57
- @targets ||= []
57
+ @criteria ||= []
58
58
  @ad_groups ||= []
59
59
 
60
60
  super(params)
@@ -81,15 +81,15 @@ module Adapi
81
81
 
82
82
  self.id = response[:value].first[:id] rescue nil
83
83
 
84
- # create targets if they are available
85
- if targets.size > 0
86
- target = Adapi::CampaignTarget.create(
84
+ # create criteria (former targets) if they are available
85
+ if criteria.size > 0
86
+ criterion = Adapi::CampaignCriterion.create(
87
87
  :campaign_id => @id,
88
- :targets => targets
88
+ :criteria => criteria
89
89
  )
90
90
 
91
- if (target.errors.size > 0)
92
- self.errors.add("[campaign target]", target.errors.to_a)
91
+ if (criterion.errors.size > 0)
92
+ self.errors.add("[campaign criterion]", criterion.errors.to_a)
93
93
  self.rollback
94
94
  return false
95
95
  end
@@ -116,9 +116,8 @@ module Adapi
116
116
  # TODO implement class method
117
117
  #
118
118
  def update(params = {})
119
- # TODO validation or refuse to update
120
-
121
- response = self.mutate(
119
+ # HOTFIX can't use current instance, gotta create new one
120
+ response = Adapi::Campaign.new().mutate(
122
121
  :operator => 'SET',
123
122
  :operand => params.merge(:id => @id)
124
123
  )
@@ -128,16 +127,24 @@ module Adapi
128
127
  # faster than self.find
129
128
  params.each_pair { |k,v| self.send("#{k}=", v) }
130
129
 
131
- true
130
+ true
132
131
  end
133
132
 
134
133
  def activate; update(:status => 'ACTIVE'); end
135
134
  def pause; update(:status => 'PAUSED'); end
135
+
136
+ # Deletes campaign - which means, sets its status to deleted (because
137
+ # AdWords campaigns are never really deleted.)
138
+ #
136
139
  def delete; update(:status => 'DELETED'); end
137
140
 
138
141
  def rename(new_name); update(:name => new_name); end
139
142
 
140
143
  # when Campaign#create fails, "delete" campaign
144
+
145
+ # Deletes campaign if it's not already deleted. For more information about
146
+ # "deleted" campaigns, see `delete` method
147
+ #
141
148
  def rollback
142
149
  if (@status == 'DELETED')
143
150
  self.errors.add(:base, 'Campaign is already deleted.')
@@ -170,7 +177,9 @@ module Adapi
170
177
 
171
178
  predicates = [ :id ].map do |param_name|
172
179
  if params[param_name]
173
- {:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
180
+ # convert to array
181
+ value = Array.try_convert(params[param_name]) ? params_param_name : [params[param_name]]
182
+ {:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
174
183
  end
175
184
  end.compact
176
185
 
@@ -200,13 +209,13 @@ module Adapi
200
209
  AdGroup.find( (first_only ? :first : :all), :campaign_id => self.id )
201
210
  end
202
211
 
203
- # Returns complete campaign data: targets, ad groups, keywords and ads.
212
+ # Returns complete campaign data: criteria, ad groups, keywords and ads.
204
213
  # Basically everything what you can set when creating a campaign.
205
214
  #
206
215
  def self.find_complete(campaign_id)
207
216
  campaign = self.find(campaign_id)
208
217
 
209
- campaign[:targets] = CampaignTarget.find(:campaign_id => campaign.to_param)
218
+ campaign[:criteria] = CampaignCriterion.find(:campaign_id => campaign.to_param)
210
219
 
211
220
  campaign[:ad_groups] = AdGroup.find(:all, :campaign_id => campaign.to_param)
212
221
 
@@ -226,7 +235,7 @@ module Adapi
226
235
  :status => self[:status],
227
236
  :budget => self[:budget],
228
237
  :bidding_strategy => self[:bidding_strategy],
229
- :targets => self[:targets],
238
+ :criteria => self[:criteria],
230
239
  :ad_groups => self[:ad_groups]
231
240
  }
232
241
  end
@@ -0,0 +1,278 @@
1
+ # encoding: utf-8
2
+
3
+ module Adapi
4
+
5
+ # http://code.google.com/apis/adwords/docs/reference/latest/CampaignTargetService.html
6
+ #
7
+ class CampaignCriterion < Api
8
+
9
+ attr_accessor :campaign_id, :criteria
10
+
11
+ validates_presence_of :campaign_id
12
+
13
+ CRITERION_TYPES = [ :age_range, :carrier, :content_label, :gender, :keyword,
14
+ :language, :location, :operating_system_version, :placement, :platform,
15
+ :polygon, :product, :proximity, :criterion_user_interest,
16
+ :criterion_user_list, :vertical ]
17
+
18
+ def attributes
19
+ super.merge( 'campaign_id' => campaign_id, 'criteria' => criteria )
20
+ end
21
+
22
+ def initialize(params = {})
23
+ params[:service_name] = :CampaignCriterionService
24
+ params[:negative] ||= false
25
+
26
+ @xsi_type = if (params[:negative] == true)
27
+ 'NegativeCampaignCriterion'
28
+ else
29
+ 'CampaignCriterion'
30
+ end
31
+
32
+ %w{ campaign_id criteria }.each do |param_name|
33
+ self.send "#{param_name}=", params[param_name.to_sym]
34
+ end
35
+
36
+ super(params)
37
+ end
38
+
39
+ def create
40
+ # step 1 - convert input hash to new array of criteria
41
+ # example: :language => [ :en, :cz ] -> [:language, :en]
42
+ criteria_array = []
43
+
44
+ @criteria.each_pair do |criterion_type, criterion_settings|
45
+ case criterion_type
46
+ when :language
47
+ criterion_settings.each do |value|
48
+ criteria_array << [criterion_type, value]
49
+ end
50
+
51
+ # location - besides standard, expected interface, this criterion is
52
+ # heavily customized to comply with legacy interfaces (pre-v201109).
53
+ #
54
+ # Standard v201109 location interface:
55
+ # :location => location_id
56
+ # :location => { :id => location_id }
57
+ # :location => { :id => [ location_id ] }
58
+ #
59
+ # Accepted subtypes:
60
+ # id
61
+ # proximity (just actually redirects to proximity criterion)
62
+ # city
63
+ # province
64
+ # country
65
+ #
66
+ when :location, :geo # PS: geo is legacy synonym for location
67
+ # handles ":location => location_id" shortcut
68
+ unless criterion_settings.is_a?(Hash)
69
+ criterion_settings = { :id => criterion_settings.to_i }
70
+ end
71
+
72
+ criterion_settings.each_pair do |subtype, subtype_settings|
73
+ # any location subtypes can be in array
74
+ subtype_settings = [ subtype_settings ] unless subtype_settings.is_a?(Array)
75
+
76
+ case subtype
77
+ when :id
78
+ subtype_settings.each do |value|
79
+ criteria_array << [:location, value]
80
+ end
81
+
82
+ # find id for location(s) by LocationCriterion service
83
+ when :name
84
+ subtype_settings = [subtype_settings] unless subtype_settings.is_a?(Array)
85
+
86
+ subtype_settings.each do |location_criteria|
87
+ location = Adapi::Location.find(location_criteria)
88
+
89
+ raise "Location not found" if location.nil?
90
+
91
+ criteria_array << [ :location, location[:id] ]
92
+ end
93
+
94
+ when :proximity
95
+ subtype_settings.each do |value|
96
+ criteria_array << [subtype, value]
97
+ end
98
+
99
+ else
100
+ raise "Unknown location subtype: %s" % subtype
101
+ end
102
+ end
103
+
104
+ # not-supported criterions (they work, but have to be entered in
105
+ # google format, no shortcuts are set up for them)
106
+ else
107
+ unless CRITERION_TYPES.include?(criterion_type)
108
+ raise "Unknown criterion type; #{criterion_type}"
109
+ end
110
+
111
+ if criterion_settings.is_a?(Array)
112
+ criterion_settings.each do |value|
113
+ criteria_array << [criterion_type, value]
114
+ end
115
+ else
116
+ criteria_array << [criterion_type, criterion_settings]
117
+ end
118
+ end
119
+ end
120
+
121
+ # step 2 - convert individual criteria to low-level google params
122
+ operations = criteria_array.map do |criterion_type, criterion_settings|
123
+ {
124
+ :operator => 'ADD',
125
+ :operand => {
126
+ :campaign_id => @campaign_id,
127
+ :criterion => CampaignCriterion::create_criterion(criterion_type, criterion_settings)
128
+ }
129
+ }
130
+ end
131
+
132
+ response = self.mutate(operations)
133
+
134
+ (response and response[:value]) ? true : false
135
+ end
136
+
137
+ def self.find(params = {})
138
+ params.symbolize_keys!
139
+
140
+ # by default, skip criteria types that have no criterion data
141
+ params[:skip_empty_criterion_types] ||= true
142
+
143
+ if params[:conditions]
144
+ params[:campaign_id] = params[:campaign_id] || params[:conditions][:campaign_id]
145
+ end
146
+
147
+ raise ArgumentError, "Campaign ID is required" unless params[:campaign_id]
148
+
149
+ predicates = [ :campaign_id ].map do |param_name|
150
+ if params[param_name]
151
+ # convert to array
152
+ value = Array.try_convert(params[param_name]) ? params_param_name : [params[param_name]]
153
+ {:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
154
+ end
155
+ end.compact
156
+
157
+ # TODO: get more fields
158
+ selector = {
159
+ :fields => ['Id'],
160
+ :ordering => [{:field => 'Id', :sort_order => 'ASCENDING'}],
161
+ :predicates => predicates
162
+ }
163
+
164
+ response = CampaignCriterion.new.service.get(selector)
165
+
166
+ response = (response and response[:entries]) ? response[:entries] : []
167
+
168
+ # return everything or only
169
+ if params[:skip_empty_criterion_types]
170
+ response.select! { |criterion_type| criterion_type.has_key?(:criteria) }
171
+ end
172
+
173
+ # TODO optionally return just certain type(s)
174
+ # easy, just add condition (single type or array), filter and set
175
+ # :skip_empty_target_types option to false
176
+
177
+ response
178
+ end
179
+
180
+ # Transforms our custom high-level criteria parameters to AdWords API parameters
181
+ #
182
+ # Every criterion can be entered as high-level alias or as id
183
+ #
184
+ # Language:
185
+ # :language => [ :en, :cs ]
186
+ # :language => [ 1000, 1021 ] # integers!
187
+ #
188
+ #
189
+ def self.create_criterion(criterion_type, criterion_data)
190
+ case criterion_type
191
+ #
192
+ # example: [:language, 'en'] -> {:xsi_type => 'Language', :id => 1000}
193
+ when :language
194
+ {
195
+ :xsi_type => 'Language',
196
+ :id => ConstantData::Language.find(criterion_data).id
197
+ }
198
+
199
+ when :location
200
+ {
201
+ :xsi_type => 'Location',
202
+ :id => criterion_data.to_i
203
+ }
204
+
205
+ when :proximity
206
+ radius_in_units, radius_units = parse_radius(criterion_data[:radius])
207
+ long, lat = parse_geodata(criterion_data[:geo_point])
208
+
209
+ {
210
+ :xsi_type => 'Proximity',
211
+ :radius_in_units => radius_in_units,
212
+ :radius_distance_units => radius_units,
213
+ :geo_point => {
214
+ :longitude_in_micro_degrees => long,
215
+ :latitude_in_micro_degrees => lat
216
+ }
217
+ }
218
+
219
+ =begin
220
+ when :city
221
+ geo_values.merge(
222
+ :xsi_type => "#{geo_type.to_s.capitalize}Target",
223
+ :excluded => false
224
+ )
225
+
226
+ else # :country, :province
227
+ {
228
+ :xsi_type => "#{geo_type.to_s.capitalize}Target",
229
+ :excluded => false,
230
+ "#{geo_type}_code".to_sym => to_uppercase(geo_values)
231
+ }
232
+ end
233
+ =end
234
+
235
+ # unsupported criterion types
236
+ else
237
+ { :xsi_type => criterion_type.to_s.camelize }.merge(criterion_data)
238
+
239
+ end
240
+ end
241
+
242
+ def self.parse_radius(radius)
243
+ radius_in_units, radius_units = radius.split(' ', 2)
244
+ [
245
+ radius_in_units.to_i,
246
+ (radius_units == 'm') ? 'MILES' : 'KILOMETERS'
247
+ ]
248
+ end
249
+
250
+ # parse longitude and lattitude from string in this format:
251
+ # "longitude,lattitude" to [int,int] in Google microdegrees
252
+ # for example: "38.89859,-77.035971" -> [38898590, -77035971]
253
+ #
254
+ def self.parse_geodata(long_lat)
255
+ long_lat.split(',', 2).map { |x| to_microdegrees(x) }
256
+ end
257
+
258
+ # convert latitude or longitude data to microdegrees,
259
+ # a format with AdWords API accepts
260
+ #
261
+ # TODO alias :to_microdegrees :to_micro_units
262
+ #
263
+ def self.to_microdegrees(x)
264
+ Api.to_micro_units(x)
265
+ end
266
+
267
+ # convert either single value or array of value to uppercase
268
+ #
269
+ def self.to_uppercase(values)
270
+ if values.is_a?(Array)
271
+ values.map { |value| value.to_s.upcase }
272
+ else
273
+ values.to_s.upcase
274
+ end
275
+ end
276
+
277
+ end
278
+ end
@@ -1,5 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
+ # This class is obsolete in v201109, CampaignCriterion is used instead. Only
4
+ # AdScheduleTarget is still being used, but it's not implemented yet.
5
+
3
6
  module Adapi
4
7
 
5
8
  # http://code.google.com/apis/adwords/docs/reference/latest/CampaignTargetService.html
@@ -10,8 +13,6 @@ module Adapi
10
13
 
11
14
  validates_presence_of :campaign_id
12
15
 
13
- # TODO validate if targets are in correct format
14
-
15
16
  def attributes
16
17
  super.merge( 'campaign_id' => campaign_id, 'targets' => targets )
17
18
  end
@@ -52,9 +53,6 @@ module Adapi
52
53
 
53
54
  def self.find(params = {})
54
55
  params.symbolize_keys!
55
-
56
- # by default, return skip target types that have no target data
57
- params[:skip_empty_target_types] ||= true
58
56
 
59
57
  if params[:conditions]
60
58
  params[:campaign_id] = params[:campaign_id] || params[:conditions][:campaign_id]
@@ -68,129 +66,13 @@ module Adapi
68
66
 
69
67
  response = (response and response[:entries]) ? response[:entries] : []
70
68
 
71
- # return everything or only
72
- if params[:skip_empty_target_types]
73
- response.select! { |target_type| target_type.has_key?(:targets) }
74
- end
75
-
76
- # TODO optionally return just certain target type(s)
77
- # easy, just add condition (single type or array), filter and set
78
- # :skip_empty_target_types option to false
79
-
80
- # optionally convert to original input shortcuts specified by Adapi DSL
81
- # TODO on second thought, no need to do that now. it's simpler to make
82
- # CampaignTarget.new accept original AdWords data
83
- # if params[:format] == :adapi_dsl
84
- # response = Hash[ response.map { |t| CampaignTarget.parse_dsl(t) } ]
85
- # end
86
-
87
69
  response
88
70
  end
89
71
 
90
- =begin TODO
91
- def self.parse_dsl(target)
92
- case target[:target_list_type]
93
- when "LanguageTargetList"
94
- [ :language, target[:targets].map { |t| t[:language_code] } ]
95
- when "GeoTargetList"
96
- targets = Hash[ target[:targets].map do |t|
97
- target_type = t[:target_type].gsub(/Target$/, '')
98
-
99
- case target_type
100
- when :proximity
101
- [ target_type, { :geo_point => '', :radius => '' } ]
102
- when :city
103
- else
104
- [ target_type, t ]
105
- end
106
- end ]
107
-
108
- [ :geo, targets ]
109
- else
110
- warn "Unsupported target type! %s" % target.inspect
111
- [ target[:target_list_type], target[:targets] ]
112
- end
113
- end
114
- =end
115
-
116
- # transform our own high-level target parameters to google low-level
117
- #
118
- # TODO allow to enter AdWords parameters in original format
72
+ # Obsolete. Transforms our own high-level target parameters to google low-level
119
73
  #
120
74
  def self.create_targets(target_type, target_data)
121
- case target_type
122
- when :language
123
- target_data.map { |language| { :language_code => language.to_s.downcase } }
124
- # example: ['cz','sk'] => [{:language_code => 'cz'}, {:language_code => 'sk'}]
125
- when :geo
126
- target_data.map do |geo_type, geo_values|
127
- case geo_type
128
- when :proximity
129
- radius_in_units, radius_units = parse_radius(geo_values[:radius])
130
- long, lat = parse_geodata(geo_values[:geo_point])
131
-
132
- {
133
- :xsi_type => "#{geo_type.to_s.capitalize}Target",
134
- :excluded => false,
135
- :radius_in_units => radius_in_units,
136
- :radius_distance_units => radius_units,
137
- :geo_point => {
138
- :longitude_in_micro_degrees => long,
139
- :latitude_in_micro_degrees => lat
140
- }
141
- }
142
-
143
- when :city
144
- geo_values.merge(
145
- :xsi_type => "#{geo_type.to_s.capitalize}Target",
146
- :excluded => false
147
- )
148
-
149
- else # :country, :province
150
- {
151
- :xsi_type => "#{geo_type.to_s.capitalize}Target",
152
- :excluded => false,
153
- "#{geo_type}_code".to_sym => to_uppercase(geo_values)
154
- }
155
- end
156
- end
157
- else nil
158
- end
159
- end
160
-
161
- def self.parse_radius(radius)
162
- radius_in_units, radius_units = radius.split(' ', 2)
163
- [
164
- radius_in_units.to_i,
165
- (radius_units == 'm') ? 'MILES' : 'KILOMETERS'
166
- ]
167
- end
168
-
169
- # parse longitude and lattitude from string in this format:
170
- # "longitude,lattitude" to [int,int] in Google microdegrees
171
- # for example: "38.89859,-77.035971" -> [38898590, -77035971]
172
- #
173
- def self.parse_geodata(long_lat)
174
- long_lat.split(',', 2).map { |x| to_microdegrees(x) }
175
- end
176
-
177
- # convert latitude or longitude data to microdegrees,
178
- # a format with AdWords API accepts
179
- #
180
- # TODO alias :to_microdegrees :to_micro_units
181
- #
182
- def self.to_microdegrees(x)
183
- Api.to_micro_units(x)
184
- end
185
-
186
- # convert either single value or array of value to uppercase
187
- #
188
- def self.to_uppercase(values)
189
- if values.is_a?(Array)
190
- values.map { |value| value.to_s.upcase }
191
- else
192
- values.to_s.upcase
193
- end
75
+ nil
194
76
  end
195
77
 
196
78
  end