adapi 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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