adapi 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +74 -39
- data/adapi.gemspec +4 -0
- data/examples/add_ad_group.rb +6 -22
- data/examples/add_bare_ad_group.rb +11 -6
- data/examples/add_bare_campaign.rb +5 -7
- data/examples/add_campaign.rb +12 -29
- data/examples/add_campaign_targets.rb +16 -7
- data/examples/add_invalid_ad_group.rb +35 -0
- data/examples/add_invalid_text_ad.rb +30 -0
- data/examples/add_keywords.rb +14 -0
- data/examples/add_text_ad.rb +23 -0
- data/examples/log_to_specific_account.rb +26 -13
- data/examples/rollback_campaign.rb +56 -0
- data/examples/update_campaign.rb +14 -15
- data/examples/update_campaign_status.rb +6 -6
- data/lib/adapi.rb +19 -1
- data/lib/adapi/ad.rb +55 -42
- data/lib/adapi/ad/text_ad.rb +126 -0
- data/lib/adapi/ad_group.rb +80 -42
- data/lib/adapi/ad_group_criterion.rb +13 -55
- data/lib/adapi/api.rb +107 -1
- data/lib/adapi/campaign.rb +144 -94
- data/lib/adapi/campaign_target.rb +93 -45
- data/lib/adapi/config.rb +16 -2
- data/lib/adapi/keyword.rb +109 -0
- data/lib/adapi/version.rb +12 -1
- data/lib/httpi_request_monkeypatch.rb +35 -0
- data/test/factories/ad_group_factory.rb +20 -0
- data/test/factories/ad_text_factory.rb +9 -0
- data/test/test_helper.rb +3 -3
- data/test/unit/ad/ad_text_test.rb +30 -0
- data/test/unit/ad_group_test.rb +34 -0
- data/test/unit/ad_test.rb +12 -0
- data/test/unit/campaign_target_test.rb +18 -3
- metadata +122 -109
- data/examples/add_ad.rb +0 -17
- data/examples/add_ad_group_criteria.rb +0 -20
- data/examples/update_campaign_name.rb +0 -14
@@ -1,67 +1,25 @@
|
|
1
1
|
module Adapi
|
2
|
-
class AdGroupCriterion < Api
|
3
|
-
|
4
|
-
def initialize(params = {})
|
5
|
-
params[:service_name] = :AdGroupCriterionService
|
6
|
-
super(params)
|
7
|
-
end
|
8
|
-
|
9
|
-
def self.create(params = {})
|
10
|
-
ad_group_criterion_service = AdGroupCriterion.new
|
11
|
-
|
12
|
-
raise "No criteria available" unless params[:criteria].is_a?(Array)
|
13
|
-
|
14
|
-
# if ad_group_id is supplied as separate parameter, include it into
|
15
|
-
# criteria
|
16
|
-
if params[:ad_group_id]
|
17
|
-
params[:criteria].map! { |c| c.merge(:ad_group_id => params[:ad_group_id].to_i) }
|
18
|
-
end
|
19
2
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
response = ad_group_criterion_service.service.mutate(operation)
|
3
|
+
# http://code.google.com/apis/adwords/docs/reference/latest/AdGroupCriterionService.html
|
4
|
+
#
|
5
|
+
class AdGroupCriterion < Api
|
25
6
|
|
26
|
-
|
27
|
-
if response and response[:value]
|
28
|
-
ad_group_criteria = response[:value]
|
29
|
-
puts "Added #{ad_group_criteria.length} criteria " # "to ad group #{ad_group_id}."
|
30
|
-
ad_group_criteria.each do |ad_group_criterion|
|
31
|
-
puts " Criterion id is #{ad_group_criterion[:criterion][:id]} and " +
|
32
|
-
"type is #{ad_group_criterion[:criterion][:"@xsi:type"]}."
|
33
|
-
end
|
34
|
-
else
|
35
|
-
puts "No criteria were added."
|
36
|
-
end
|
7
|
+
attr_accessor :ad_group_id, :criterion_use
|
37
8
|
|
38
|
-
|
9
|
+
def attributes
|
10
|
+
super.merge('ad_group_id' => ad_group_id)
|
39
11
|
end
|
40
12
|
|
41
|
-
def
|
42
|
-
|
43
|
-
campaign_id = params[:campaign_id]
|
44
|
-
|
45
|
-
selector = {
|
46
|
-
:fields => ['Id', 'Name'],
|
47
|
-
# :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
|
48
|
-
:predicates => [{
|
49
|
-
:field => 'CampaignId', :operator => 'EQUALS', :values => campaign_id
|
50
|
-
}]
|
51
|
-
}
|
13
|
+
def initialize(params = {})
|
14
|
+
params[:service_name] = :AdGroupCriterionService
|
52
15
|
|
53
|
-
|
16
|
+
@xsi_type = 'AdGroupCriterion'
|
54
17
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
ad_groups.each do |ad_group|
|
59
|
-
puts " Ad group name is \"#{ad_group[:name]}\" and id is #{ad_group[:id]}."
|
60
|
-
end
|
61
|
-
else
|
62
|
-
puts "No ad groups found for campaign ##{campaign_id}."
|
63
|
-
end
|
18
|
+
%w{ ad_group_id criterion_use }.each do |param_name|
|
19
|
+
self.send "#{param_name}=", params[param_name.to_sym]
|
20
|
+
end
|
64
21
|
|
22
|
+
super(params)
|
65
23
|
end
|
66
24
|
|
67
25
|
end
|
data/lib/adapi/api.rb
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
module Adapi
|
2
2
|
class Api
|
3
|
+
extend ActiveModel::Naming
|
4
|
+
include ActiveModel::Validations
|
5
|
+
include ActiveModel::Serialization
|
6
|
+
include ActiveModel::Conversion
|
7
|
+
# TODO include ActiveModel::Dirty
|
3
8
|
|
4
|
-
attr_accessor :adwords, :service, :version, :params
|
9
|
+
attr_accessor :adwords, :service, :version, :params,
|
10
|
+
:id, :status, :xsi_type
|
11
|
+
|
12
|
+
def attributes
|
13
|
+
{ 'status' => status, 'xsi_type' => xsi_type }
|
14
|
+
end
|
5
15
|
|
6
16
|
def initialize(params = {})
|
17
|
+
params.symbolize_keys!
|
18
|
+
|
7
19
|
raise "Missing Service Name" unless params[:service_name]
|
8
20
|
|
9
21
|
puts "\n\nEXISTING INSTANCE USED\n\n" if params[:adwords_api_instance]
|
@@ -16,5 +28,99 @@ module Adapi
|
|
16
28
|
@params = params
|
17
29
|
end
|
18
30
|
|
31
|
+
def persisted?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
# FIXME hotfix, should be able to sort it out better through ActiveModel
|
36
|
+
def [](k)
|
37
|
+
self.send(k)
|
38
|
+
end
|
39
|
+
|
40
|
+
def []=(k,v)
|
41
|
+
self.send("#{k}=", v)
|
42
|
+
end
|
43
|
+
|
44
|
+
# return parameters in hash
|
45
|
+
# filtered for API calls by default: without :id and :status parameters
|
46
|
+
# PS: attributes method always returns all specified attributes
|
47
|
+
#
|
48
|
+
def data(filtered = true)
|
49
|
+
data_hash = self.serializable_hash.symbolize_keys
|
50
|
+
|
51
|
+
if filtered
|
52
|
+
data_hash.delete(:id)
|
53
|
+
data_hash.delete(:status)
|
54
|
+
end
|
55
|
+
|
56
|
+
data_hash
|
57
|
+
end
|
58
|
+
|
59
|
+
# alias to instance method: data
|
60
|
+
#
|
61
|
+
alias :to_hash :data
|
62
|
+
|
63
|
+
# detects whether the instance has been saved already
|
64
|
+
#
|
65
|
+
def new?
|
66
|
+
self.id.blank?
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.create(params = {})
|
70
|
+
api_instance = self.new(params)
|
71
|
+
api_instance.create
|
72
|
+
api_instance
|
73
|
+
end
|
74
|
+
|
75
|
+
# done mostly for campaign, probably won't work pretty much anywhere else
|
76
|
+
# which can be easily fixed creating by self.update method for specific
|
77
|
+
# class
|
78
|
+
#
|
79
|
+
def self.update(params = {})
|
80
|
+
# PS: updating campaign without finding it is much faster
|
81
|
+
api_instance = self.new()
|
82
|
+
api_instance.id = params.delete(:id)
|
83
|
+
api_instance.errors.add('id', 'is missing') unless api_instance.id
|
84
|
+
|
85
|
+
api_instance.update(params)
|
86
|
+
api_instance
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# wrap AdWords add/update/destroy actions and deals with errors
|
91
|
+
# PS: Keyword and Ad models have their own wrappers because of
|
92
|
+
# PolicyViolations
|
93
|
+
#
|
94
|
+
def mutate(operation)
|
95
|
+
operation = [operation] unless operation.is_a?(Array)
|
96
|
+
|
97
|
+
# fix to save space during specifyng operations
|
98
|
+
operation = operation.map do |op|
|
99
|
+
op[:operand].delete(:status) if op[:operand][:status].nil?
|
100
|
+
op
|
101
|
+
end
|
102
|
+
|
103
|
+
begin
|
104
|
+
response = @service.mutate(operation)
|
105
|
+
|
106
|
+
rescue AdsCommon::Errors::HttpError => e
|
107
|
+
self.errors.add(:base, e.message)
|
108
|
+
|
109
|
+
# traps any exceptions raised by AdWords API
|
110
|
+
rescue AdwordsApi::Errors::ApiException => e
|
111
|
+
error_key = "[#{self.xsi_type.underscore}]"
|
112
|
+
|
113
|
+
self.errors.add(error_key, e.message)
|
114
|
+
end
|
115
|
+
|
116
|
+
response
|
117
|
+
end
|
118
|
+
|
119
|
+
# convert number to micro units (unit * one million)
|
120
|
+
#
|
121
|
+
def self.to_micro_units(x)
|
122
|
+
(x.to_f * 1e6).to_i
|
123
|
+
end
|
124
|
+
|
19
125
|
end
|
20
126
|
end
|
data/lib/adapi/campaign.rb
CHANGED
@@ -1,141 +1,191 @@
|
|
1
1
|
module Adapi
|
2
2
|
class Campaign < Api
|
3
3
|
|
4
|
+
# http://code.google.com/apis/adwords/docs/reference/latest/CampaignService.Campaign.html
|
5
|
+
#
|
6
|
+
attr_accessor :name, :serving_status, :start_date, :end_date, :budget,
|
7
|
+
:bidding_strategy, :network_setting, :targets, :ad_groups
|
8
|
+
|
9
|
+
def attributes
|
10
|
+
super.merge('name' => name, 'start_date' => start_date, 'end_date' => end_date,
|
11
|
+
'budget' => budget, 'bidding_strategy' => bidding_strategy,
|
12
|
+
'network_setting' => network_setting, 'targets' => targets,
|
13
|
+
'ad_groups' => ad_groups)
|
14
|
+
end
|
15
|
+
|
16
|
+
validates_presence_of :name, :status
|
17
|
+
validates_inclusion_of :status, :in => %w{ ACTIVE DELETED PAUSED }
|
18
|
+
|
4
19
|
def initialize(params = {})
|
5
20
|
params[:service_name] = :CampaignService
|
6
|
-
|
7
|
-
|
21
|
+
|
22
|
+
@xsi_type = 'Campaign'
|
8
23
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
def self.create(params = {})
|
15
|
-
campaign_service = Campaign.new
|
24
|
+
%w{ name status start_date end_date budget bidding_strategy
|
25
|
+
network_setting targets ad_groups}.each do |param_name|
|
26
|
+
self.send "#{param_name}=", params[param_name.to_sym]
|
27
|
+
end
|
16
28
|
|
17
|
-
#
|
18
|
-
|
29
|
+
# convert bidding_strategy to GoogleApi
|
30
|
+
# can be either string (just xsi_type) or hash (xsi_type with params)
|
31
|
+
# TODO validations for xsi_type
|
32
|
+
#
|
33
|
+
unless @bidding_strategy.is_a?(Hash)
|
34
|
+
@bidding_strategy = { :xsi_type => @bidding_strategy }
|
35
|
+
end
|
19
36
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
operation = { :operator => 'ADD', :operand => params[:data] }
|
25
|
-
|
26
|
-
response = campaign_service.service.mutate([operation])
|
27
|
-
|
28
|
-
campaign = nil
|
29
|
-
if response and response[:value]
|
30
|
-
campaign = response[:value].first
|
31
|
-
puts "Campaign with name '%s' and ID %d was added." % [campaign[:name], campaign[:id]]
|
32
|
-
else
|
33
|
-
return nil
|
37
|
+
if @bidding_strategy[:bid_ceiling]
|
38
|
+
@bidding_strategy[:bid_ceiling] = {
|
39
|
+
:micro_amount => Api.to_micro_units(@bidding_strategy[:bid_ceiling])
|
40
|
+
}
|
34
41
|
end
|
35
42
|
|
43
|
+
# convert budget to GoogleApi
|
44
|
+
# TODO validations for budget
|
45
|
+
#
|
46
|
+
# budget can be integer (amount) or hash
|
47
|
+
@budget = { :amount => @budget } unless @budget.is_a?(Hash)
|
48
|
+
@budget[:period] ||= 'DAILY'
|
49
|
+
@budget[:amount] = { :micro_amount => Api.to_micro_units(@budget[:amount]) }
|
50
|
+
# PS: not sure if this should be a default. maybe we don't even need it
|
51
|
+
@budget[:delivery_method] ||= 'STANDARD'
|
52
|
+
|
53
|
+
@targets ||= []
|
54
|
+
@ad_groups ||= []
|
55
|
+
|
56
|
+
super(params)
|
57
|
+
end
|
58
|
+
|
59
|
+
# create campaign with ad_groups and ads
|
60
|
+
#
|
61
|
+
def create
|
62
|
+
return false unless self.valid?
|
63
|
+
|
64
|
+
operand = Hash[
|
65
|
+
[ :name, :status, :start_date, :end_date,
|
66
|
+
:budget, :bidding_strategy, :network_setting ].map do |k|
|
67
|
+
[ k.to_sym, self.send(k) ] if self.send(k)
|
68
|
+
end.compact
|
69
|
+
]
|
70
|
+
|
71
|
+
response = self.mutate(
|
72
|
+
:operator => 'ADD',
|
73
|
+
:operand => operand
|
74
|
+
)
|
75
|
+
|
76
|
+
return false unless (response and response[:value])
|
77
|
+
|
78
|
+
self.id = response[:value].first[:id] rescue nil
|
79
|
+
|
36
80
|
# create targets if they are available
|
37
|
-
if targets
|
38
|
-
Adapi::CampaignTarget.create(
|
39
|
-
:campaign_id =>
|
40
|
-
:targets => targets
|
41
|
-
:api_adwords_instance => campaign_service.adwords
|
81
|
+
if targets.size > 0
|
82
|
+
target = Adapi::CampaignTarget.create(
|
83
|
+
:campaign_id => @id,
|
84
|
+
:targets => targets
|
42
85
|
)
|
86
|
+
|
87
|
+
if (target.errors.size > 0)
|
88
|
+
self.errors.add("[campaign target]", target.errors.to_a)
|
89
|
+
self.rollback
|
90
|
+
return false
|
91
|
+
end
|
43
92
|
end
|
44
93
|
|
45
|
-
# if campaign has ad_groups, create them as well
|
46
94
|
ad_groups.each do |ad_group_data|
|
47
|
-
Adapi::AdGroup.create(
|
48
|
-
|
49
|
-
:api_adwords_instance => campaign_service.adwords
|
95
|
+
ad_group = Adapi::AdGroup.create(
|
96
|
+
ad_group_data.merge(:campaign_id => @id)
|
50
97
|
)
|
98
|
+
|
99
|
+
if (ad_group.errors.size > 0)
|
100
|
+
self.errors.add("[ad group] \"#{ad_group.name}\"", ad_group.errors.to_a)
|
101
|
+
self.rollback
|
102
|
+
return false
|
103
|
+
end
|
51
104
|
end
|
52
105
|
|
53
|
-
|
106
|
+
return true
|
54
107
|
end
|
55
108
|
|
56
109
|
# general method for changing campaign data
|
57
110
|
# TODO enable updating of all campaign parts at once, same as for Campaign#create method
|
111
|
+
#
|
112
|
+
# TODO implement class method
|
58
113
|
#
|
59
|
-
def
|
60
|
-
|
114
|
+
def update(params = {})
|
115
|
+
# TODO validation or refuse to update
|
61
116
|
|
62
|
-
|
63
|
-
|
117
|
+
response = self.mutate(
|
118
|
+
:operator => 'SET',
|
119
|
+
:operand => params.merge(:id => @id)
|
120
|
+
)
|
64
121
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
:operand => params[:data].merge(:id => campaign_id.to_i)
|
70
|
-
}
|
71
|
-
|
72
|
-
response = campaign_service.service.mutate([operation])
|
73
|
-
|
74
|
-
if response and response[:value]
|
75
|
-
campaign = response[:value].first
|
76
|
-
puts 'Campaign id %d successfully updated.' % campaign[:id]
|
77
|
-
else
|
78
|
-
puts 'No campaigns were updated.'
|
79
|
-
end
|
122
|
+
return false unless (response and response[:value])
|
123
|
+
|
124
|
+
# faster than self.find
|
125
|
+
params.each_pair { |k,v| self.send("#{k}=", v) }
|
80
126
|
|
81
|
-
|
127
|
+
true
|
82
128
|
end
|
83
129
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
return nil unless %w{ ACTIVE PAUSED DELETED }.include?(params[:status])
|
130
|
+
def activate; update(:status => 'ACTIVE'); end
|
131
|
+
def pause; update(:status => 'PAUSED'); end
|
132
|
+
def delete; update(:status => 'DELETED'); end
|
88
133
|
|
89
|
-
|
90
|
-
end
|
134
|
+
def rename(new_name); update(:name => new_name); end
|
91
135
|
|
92
|
-
|
93
|
-
|
94
|
-
|
136
|
+
# when Campaign#create fails, "delete" campaign
|
137
|
+
def rollback
|
138
|
+
if (@status == 'DELETED')
|
139
|
+
self.errors.add(:base, 'Campaign is already deleted.')
|
140
|
+
return false
|
141
|
+
end
|
95
142
|
|
96
|
-
|
97
|
-
|
143
|
+
update(
|
144
|
+
:name => "#{@name}_DELETED_#{(Time.now.to_f * 1000).to_i}",
|
145
|
+
:status => 'DELETED'
|
146
|
+
)
|
98
147
|
end
|
99
148
|
|
100
|
-
def
|
101
|
-
|
149
|
+
def find # == refresh
|
150
|
+
Campaign.find(:first, :id => @id)
|
102
151
|
end
|
103
152
|
|
104
|
-
def self.
|
105
|
-
params
|
106
|
-
|
153
|
+
def self.find(amount = :all, params = {})
|
154
|
+
params.symbolize_keys!
|
155
|
+
first_only = (amount.to_sym == :first)
|
107
156
|
|
108
|
-
|
109
|
-
end
|
157
|
+
raise "Campaign ID (:id param) is required" unless params[:id]
|
110
158
|
|
111
|
-
|
112
|
-
|
159
|
+
predicates = [ :id ].map do |param_name|
|
160
|
+
if params[param_name]
|
161
|
+
{:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
|
162
|
+
end
|
163
|
+
end.compact
|
113
164
|
|
165
|
+
# TODO display the rest of the data
|
166
|
+
# TODO get NetworkSetting - setting as in fields doesn't work
|
114
167
|
selector = {
|
115
|
-
:fields => ['Id', 'Name', 'Status']
|
116
|
-
|
117
|
-
|
168
|
+
:fields => ['Id', 'Name', 'Status', 'BiddingStrategy' ],
|
169
|
+
:ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
|
170
|
+
:predicates => predicates
|
118
171
|
}
|
119
172
|
|
120
|
-
|
121
|
-
if params[:conditions]
|
122
|
-
selector[:predicates] = params[:conditions].map do |c|
|
123
|
-
{ :field => c[0].to_s.capitalize, :operator => 'EQUALS', :values => c[1] }
|
124
|
-
end
|
125
|
-
end
|
173
|
+
response = Campaign.new.service.get(selector)
|
126
174
|
|
127
|
-
response =
|
175
|
+
response = (response and response[:entries]) ? response[:entries] : []
|
128
176
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
"and status is \"#{campaign[:status]}\"."
|
135
|
-
end
|
136
|
-
else
|
137
|
-
puts "No campaigns were found."
|
177
|
+
response.map! do |campaign_data|
|
178
|
+
campaign = Campaign.new(campaign_data)
|
179
|
+
# TODO allow mass assignment of :id
|
180
|
+
campaign.id = campaign_data[:id]
|
181
|
+
campaign
|
138
182
|
end
|
183
|
+
|
184
|
+
first_only ? response.first : response
|
185
|
+
end
|
186
|
+
|
187
|
+
def find_ad_groups(first_only = true)
|
188
|
+
AdGroup.find( (first_only ? :first : :all), :campaign_id => self.id )
|
139
189
|
end
|
140
190
|
|
141
191
|
end
|