adapi 0.0.2 → 0.0.3
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.
- 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
|