adapi 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +3 -0
  2. data/README.markdown +176 -0
  3. data/Rakefile +32 -0
  4. data/adapi.gemspec +7 -4
  5. data/examples/add_ad_group.rb +1 -0
  6. data/examples/add_bare_ad_group.rb +2 -6
  7. data/examples/add_bare_campaign.rb +1 -0
  8. data/examples/add_campaign.rb +1 -0
  9. data/examples/add_campaign_targets.rb +1 -0
  10. data/examples/add_invalid_ad_group.rb +1 -0
  11. data/examples/add_keywords.rb +13 -1
  12. data/examples/custom_settings.yml +2 -2
  13. data/examples/customize_configuration.rb +2 -0
  14. data/examples/delete_keyword.rb +27 -0
  15. data/examples/find_all_campaigns.rb +13 -0
  16. data/examples/find_campaign.rb +39 -0
  17. data/examples/find_campaign_ad_groups.rb +25 -0
  18. data/examples/log_to_specific_account.rb +1 -0
  19. data/examples/rollback_campaign.rb +1 -0
  20. data/lib/adapi.rb +8 -5
  21. data/lib/adapi/ad.rb +2 -0
  22. data/lib/adapi/ad/text_ad.rb +15 -0
  23. data/lib/adapi/ad_group.rb +45 -6
  24. data/lib/adapi/ad_group_criterion.rb +14 -0
  25. data/lib/adapi/api.rb +6 -0
  26. data/lib/adapi/campaign.rb +48 -5
  27. data/lib/adapi/campaign_target.rb +67 -8
  28. data/lib/adapi/config.rb +1 -0
  29. data/lib/adapi/keyword.rb +78 -11
  30. data/lib/adapi/version.rb +9 -1
  31. data/lib/httpi_request_monkeypatch.rb +1 -0
  32. data/test/factories/ad_group_factory.rb +4 -13
  33. data/test/factories/ad_text_factory.rb +1 -0
  34. data/test/test_helper.rb +3 -0
  35. data/test/unit/ad/ad_text_test.rb +2 -0
  36. data/test/unit/ad_group_test.rb +11 -2
  37. data/test/unit/ad_test.rb +2 -0
  38. data/test/unit/campaign_target_test.rb +15 -0
  39. metadata +67 -32
  40. data/README.rdoc +0 -162
  41. data/lib/collection.rb +0 -429
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
 
2
3
  require 'adapi'
3
4
 
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
 
2
3
  require 'adapi'
3
4
 
@@ -1,7 +1,7 @@
1
+ # encoding: utf-8
1
2
 
2
3
  require 'rubygems'
3
4
  require 'adwords_api'
4
- require 'collection'
5
5
  require 'yaml'
6
6
  require 'pp'
7
7
 
@@ -27,10 +27,13 @@ HTTPI.adapter = :curb
27
27
  # supress HTTPI output
28
28
  # HTTPI.log = false
29
29
 
30
- # load factories for development environment
31
- # require 'factory_girl'
32
- # Dir[ File.join(File.dirname(__FILE__), '../test/factories/*.rb') ].each { |f| require f }
33
-
34
30
  module Adapi
35
31
  API_VERSION = :v201101
36
32
  end
33
+
34
+ # check ruby version, should be 1.9
35
+ # FIXME there's gotta be more elegant way to do this
36
+ `ruby -v`.to_s =~ /^ruby (\d\.\d)\./
37
+ if $1.to_f < 1.9
38
+ puts "WARNING: please use ruby 1.9, adapi gem won't work properly in 1.8 and earlier versions"
39
+ end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
 
3
5
  # Ad == AdGroupAd
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
  # Ad::TextAd == AdGroupAd::TextAd
3
5
  #
@@ -122,5 +124,18 @@ module Adapi
122
124
  first_only ? response.first : response
123
125
  end
124
126
 
127
+ # Converts text ad data to hash - of the same structure which is used when
128
+ # creating a complete campaign.
129
+ #
130
+ def to_hash
131
+ {
132
+ :headline => self[:headline],
133
+ :description1 => self[:description1],
134
+ :description2 => self[:description2],
135
+ :url => self[:url],
136
+ :display_url => self[:display_url]
137
+ }
138
+ end
139
+
125
140
  end
126
141
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
  class AdGroup < Api
3
5
 
@@ -19,6 +21,25 @@ module Adapi
19
21
  self.send "#{param_name}=", params[param_name.to_sym]
20
22
  end
21
23
 
24
+ # convert bids to GoogleApi format
25
+ #
26
+ # can be either string (just xsi_type) or hash (xsi_type with params)
27
+ # althogh I'm not sure if just string makes sense in this case
28
+ #
29
+ if @bids
30
+ unless @bids.is_a?(Hash)
31
+ @bids = { :xsi_type => @bids }
32
+ end
33
+
34
+ if @bids[:keyword_max_cpc] and not @bids[:keyword_max_cpc].is_a?(Hash)
35
+ @bids[:keyword_max_cpc] = {
36
+ :amount => {
37
+ :micro_amount => Api.to_micro_units(@bids[:keyword_max_cpc])
38
+ }
39
+ }
40
+ end
41
+ end
42
+
22
43
  @keywords ||= []
23
44
  @ads ||= []
24
45
 
@@ -71,7 +92,7 @@ module Adapi
71
92
  params.symbolize_keys!
72
93
  first_only = (amount.to_sym == :first)
73
94
 
74
- raise "No Campaign ID is required" unless params[:campaign_id]
95
+ raise "Campaign ID is required" unless params[:campaign_id]
75
96
 
76
97
  predicates = [ :campaign_id, :id ].map do |param_name|
77
98
  if params[param_name]
@@ -87,22 +108,40 @@ module Adapi
87
108
 
88
109
  response = AdGroup.new.service.get(selector)
89
110
 
90
- response = (response and response[:entries]) ? response[:entries] : []
111
+ ad_groups = (response and response[:entries]) ? response[:entries] : []
91
112
 
92
- #response.map! do |data|
93
- # TextAd.new(data[:ad].merge(:ad_group_id => data[:ad_group_id], :status => data[:status]))
94
- #end
113
+ ad_groups = ad_groups.slice(0,1) if first_only
95
114
 
96
- first_only ? response.first : response
115
+ # find keywords and ads
116
+ ad_groups.map! do |ad_group|
117
+ ad_group.merge(
118
+ :keywords => Keyword.shortened(Keyword.find(:all, :ad_group_id => ad_group[:id]).keywords),
119
+ :ads => Ad::TextAd.find(:all, :ad_group_id => ad_group[:id]).map(&:to_hash)
120
+ )
121
+ end
122
+
123
+ first_only ? ad_groups.first : ad_groups
97
124
  end
98
125
 
99
126
  def find_keywords(first_only = false)
100
127
  Keyword.find( (first_only ? :first : :all), :ad_group_id => self.id )
101
128
  end
102
129
 
130
+ # TODO find all types of ads
103
131
  def find_ads(first_only = false)
104
132
  Ad::TextAd.find( (first_only ? :first : :all), :ad_group_id => self.id )
105
133
  end
106
134
 
135
+ # Converts ad group data to hash - of the same structure which is used when
136
+ # creating an ad group.
137
+ #
138
+ def to_hash
139
+ ad_group_hash = {
140
+ :id => self[:id],
141
+ :name => self[:name],
142
+ :status => self[:status]
143
+ }
144
+ end
145
+
107
146
  end
108
147
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
 
3
5
  # http://code.google.com/apis/adwords/docs/reference/latest/AdGroupCriterionService.html
@@ -22,5 +24,17 @@ module Adapi
22
24
  super(params)
23
25
  end
24
26
 
27
+ def delete(criterion_id)
28
+ response = self.mutate(
29
+ :operator => 'REMOVE',
30
+ :operand => {
31
+ :ad_group_id => self.ad_group_id,
32
+ :criterion => { :id => criterion_id.to_i }
33
+ }
34
+ )
35
+
36
+ (response and response[:value])
37
+ end
38
+
25
39
  end
26
40
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
  class Api
3
5
  extend ActiveModel::Naming
@@ -28,6 +30,10 @@ module Adapi
28
30
  @params = params
29
31
  end
30
32
 
33
+ def to_param
34
+ self[:id]
35
+ end
36
+
31
37
  def persisted?
32
38
  false
33
39
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
  class Campaign < Api
3
5
 
@@ -34,7 +36,7 @@ module Adapi
34
36
  @bidding_strategy = { :xsi_type => @bidding_strategy }
35
37
  end
36
38
 
37
- if @bidding_strategy[:bid_ceiling]
39
+ if @bidding_strategy[:bid_ceiling] and not @bidding_strategy[:bid_ceiling].is_a?(Hash)
38
40
  @bidding_strategy[:bid_ceiling] = {
39
41
  :micro_amount => Api.to_micro_units(@bidding_strategy[:bid_ceiling])
40
42
  }
@@ -46,7 +48,9 @@ module Adapi
46
48
  # budget can be integer (amount) or hash
47
49
  @budget = { :amount => @budget } unless @budget.is_a?(Hash)
48
50
  @budget[:period] ||= 'DAILY'
49
- @budget[:amount] = { :micro_amount => Api.to_micro_units(@budget[:amount]) }
51
+ if @budget[:amount] and not @budget[:amount].is_a?(Hash)
52
+ @budget[:amount] = { :micro_amount => Api.to_micro_units(@budget[:amount]) }
53
+ end
50
54
  # PS: not sure if this should be a default. maybe we don't even need it
51
55
  @budget[:delivery_method] ||= 'STANDARD'
52
56
 
@@ -150,12 +154,20 @@ module Adapi
150
154
  Campaign.find(:first, :id => @id)
151
155
  end
152
156
 
157
+
158
+ # if nothing else than single number or string at the input, assume it's an
159
+ # id and we want to find campaign by id
160
+ #
153
161
  def self.find(amount = :all, params = {})
162
+ # find campaign by id - related syntactic sugar
163
+ if params.empty? and not amount.is_a?(Symbol)
164
+ params[:id] = amount.to_i
165
+ amount = :first
166
+ end
167
+
154
168
  params.symbolize_keys!
155
169
  first_only = (amount.to_sym == :first)
156
170
 
157
- raise "Campaign ID (:id param) is required" unless params[:id]
158
-
159
171
  predicates = [ :id ].map do |param_name|
160
172
  if params[param_name]
161
173
  {:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
@@ -165,7 +177,7 @@ module Adapi
165
177
  # TODO display the rest of the data
166
178
  # TODO get NetworkSetting - setting as in fields doesn't work
167
179
  selector = {
168
- :fields => ['Id', 'Name', 'Status', 'BiddingStrategy' ],
180
+ :fields => ['Id', 'Name', 'Status', 'BiddingStrategy'],
169
181
  :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
170
182
  :predicates => predicates
171
183
  }
@@ -188,5 +200,36 @@ module Adapi
188
200
  AdGroup.find( (first_only ? :first : :all), :campaign_id => self.id )
189
201
  end
190
202
 
203
+ # Returns complete campaign data: targets, ad groups, keywords and ads.
204
+ # Basically everything what you can set when creating a campaign.
205
+ #
206
+ def self.find_complete(campaign_id)
207
+ campaign = self.find(campaign_id)
208
+
209
+ campaign[:targets] = CampaignTarget.find(:campaign_id => campaign.to_param)
210
+
211
+ campaign[:ad_groups] = AdGroup.find(:all, :campaign_id => campaign.to_param)
212
+
213
+ campaign
214
+ end
215
+
216
+ # Converts campaign data to hash - of the same structure which is used when
217
+ # creating a campaign.
218
+ #
219
+ # PS: could be implemented more succintly, but let's leave it like this for
220
+ # now, code can change and this is more readable
221
+ #
222
+ def to_hash
223
+ {
224
+ :id => self[:id],
225
+ :name => self[:name],
226
+ :status => self[:status],
227
+ :budget => self[:budget],
228
+ :bidding_strategy => self[:bidding_strategy],
229
+ :targets => self[:targets],
230
+ :ad_groups => self[:ad_groups]
231
+ }
232
+ end
233
+
191
234
  end
192
235
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  module Adapi
2
4
 
3
5
  # http://code.google.com/apis/adwords/docs/reference/latest/CampaignTargetService.html
@@ -48,12 +50,15 @@ module Adapi
48
50
 
49
51
  alias :create :set
50
52
 
51
- # FIXME doesn't display everything, check the issues in google-adwords-api
52
- #
53
- def self.find(amount = :all, params = {})
53
+ def self.find(params = {})
54
54
  params.symbolize_keys!
55
- params = params[:conditions] if params[:conditions]
56
- first_only = (amount.to_sym == :first)
55
+
56
+ # by default, return skip target types that have no target data
57
+ params[:skip_empty_target_types] ||= true
58
+
59
+ if params[:conditions]
60
+ params[:campaign_id] = params[:campaign_id] || params[:conditions][:campaign_id]
61
+ end
57
62
 
58
63
  raise ArgumentError, "Campaing ID is required" unless params[:campaign_id]
59
64
 
@@ -63,15 +68,59 @@ module Adapi
63
68
 
64
69
  response = (response and response[:entries]) ? response[:entries] : []
65
70
 
66
- first_only ? response.first : response
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
+ response
67
88
  end
68
89
 
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
+
69
116
  # transform our own high-level target parameters to google low-level
117
+ #
118
+ # TODO allow to enter AdWords parameters in original format
70
119
  #
71
120
  def self.create_targets(target_type, target_data)
72
121
  case target_type
73
122
  when :language
74
- target_data.map { |language| { :language_code => language } }
123
+ target_data.map { |language| { :language_code => language.to_s.downcase } }
75
124
  # example: ['cz','sk'] => [{:language_code => 'cz'}, {:language_code => 'sk'}]
76
125
  when :geo
77
126
  target_data.map do |geo_type, geo_values|
@@ -101,7 +150,7 @@ module Adapi
101
150
  {
102
151
  :xsi_type => "#{geo_type.to_s.capitalize}Target",
103
152
  :excluded => false,
104
- "#{geo_type}_code".to_sym => geo_values
153
+ "#{geo_type}_code".to_sym => to_uppercase(geo_values)
105
154
  }
106
155
  end
107
156
  end
@@ -134,5 +183,15 @@ module Adapi
134
183
  Api.to_micro_units(x)
135
184
  end
136
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
194
+ end
195
+
137
196
  end
138
197
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
 
2
3
  # PS: what about this config setting?
3
4
  # Campaign.create(:data => campaign_data, :account => :my_account_alias)
@@ -1,3 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ # TODO user should be able to delete keywords
4
+ # TODO user should not see deleted keywords by default
5
+ #
6
+ # TODO program should be able to detect keywords in shortened or Google form
7
+ # automatically on input (outsource into separate method?)
8
+ #
9
+ # TODO user should be able to enter keywords in both shortened, parameterized and Google form
10
+ #
11
+ # FIXME broken Keyword.negative param
12
+
13
+ # Currently the Keyword DSL is a mess. There are basically three forms:
14
+ # * ultra short form on input: keyword example
15
+ # * shortened form: {:text=>"keyword example", :match_type=>"BROAD", :negative=>false}
16
+ # * google form
17
+
1
18
  module Adapi
2
19
  class Keyword < AdGroupCriterion
3
20
 
@@ -22,22 +39,22 @@ module Adapi
22
39
  super(params)
23
40
  end
24
41
 
25
- # TODO include formatting in create method
42
+ # Converts keyword specification from shortened form to Google format
26
43
  #
27
44
  def self.keyword_attributes(keyword)
28
45
  # detect match type
29
46
  match_type = case keyword[0]
30
- when '"'
31
- keyword = keyword.slice(1, (keyword.size - 2))
32
- 'PHRASE'
33
- when '['
34
- keyword = keyword.slice(1, (keyword.size - 2))
35
- 'EXACT'
36
- else
37
- 'BROAD'
47
+ when '"'
48
+ keyword = keyword.slice(1, (keyword.size - 2))
49
+ 'PHRASE'
50
+ when '['
51
+ keyword = keyword.slice(1, (keyword.size - 2))
52
+ 'EXACT'
53
+ else
54
+ 'BROAD'
38
55
  end
39
56
 
40
- # detect if keyword is negative
57
+ # sets whether keyword is negative or not
41
58
  negative = if (keyword =~ /^\-/)
42
59
  keyword.slice!(0, 1)
43
60
  true
@@ -74,6 +91,8 @@ module Adapi
74
91
  end
75
92
 
76
93
  def self.find(amount = :all, params = {})
94
+ params[:format] ||= :google # default, don't do anything with the data from google
95
+
77
96
  params.symbolize_keys!
78
97
  # this has no effect, it's here just to have the same interface everywhere
79
98
  first_only = (amount.to_sym == :first)
@@ -99,11 +118,59 @@ module Adapi
99
118
 
100
119
  response = (response and response[:entries]) ? response[:entries] : []
101
120
 
121
+ # for now, always return keywords in :google format
122
+ =begin
123
+ response = case params[:format].to_sym
124
+ when :short
125
+ Keyword.shortened(response)
126
+ when :params
127
+ Keyword.parameterized(response)
128
+ else
129
+ response
130
+ end
131
+ =end
132
+
102
133
  Keyword.new(
103
134
  :ad_group_id => params[:ad_group_id],
104
- :keywords => response.map { |keyword| keyword[:criterion] }
135
+ :keywords => response
105
136
  )
106
137
  end
107
138
 
139
+ # PS: create a better UI for this?
140
+ # Keyword.convert(:to => :params, :source => $google_keywords)
141
+ # and Keyword.parametrized($google_keywords) just calling that?
142
+
143
+ # Converts list of keywords from Google format to short format
144
+ #
145
+ def self.shortened(google_keywords = [])
146
+ self.parameterized(google_keywords).map do |keyword|
147
+ keyword[:text] = "-%s" % keyword[:text] if keyword[:negative]
148
+
149
+ case keyword[:match_type]
150
+ when 'PHRASE'
151
+ "\"%s\"" % keyword[:text]
152
+ when 'EXACT'
153
+ "[%s]" % keyword[:text]
154
+ else # 'BROAD'
155
+ keyword[:text]
156
+ end
157
+ end
158
+ end
159
+
160
+ # Converts list of keywords from Google format to params format
161
+ # (the way it can be entered into Keywords model)
162
+ #
163
+ def self.parameterized(google_keywords = [])
164
+ google_keywords.map do |keyword|
165
+ kw = keyword[:text][:criterion]
166
+
167
+ {
168
+ :text => kw[:text],
169
+ :match_type => kw[:match_type],
170
+ :negative => (keyword[:text][:xsi_type] == "NegativeAdGroupCriterion")
171
+ }
172
+ end
173
+ end
174
+
108
175
  end
109
176
  end