adapi 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- operation = params[:criteria].map do |criterion|
21
- { :operator => 'ADD', :operand => criterion }
22
- end
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
- ad_group_criteria = nil
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
- ad_group_criteria
9
+ def attributes
10
+ super.merge('ad_group_id' => ad_group_id)
39
11
  end
40
12
 
41
- def find(params = {})
42
- raise "No Campaign ID" unless params[:campaign_id]
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
- response = @service.get(selector)
16
+ @xsi_type = 'AdGroupCriterion'
54
17
 
55
- if response and response[:entries]
56
- ad_groups = response[:entries]
57
- puts "Campaign ##{campaign_id} has #{ad_groups.length} ad group(s)."
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
@@ -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
@@ -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
- super(params)
7
- end
21
+
22
+ @xsi_type = 'Campaign'
8
23
 
9
- # campaign data can be passed either as single hash:
10
- # Campaign.create(:name => 'Campaign 123', :status => 'ENABLED')
11
- # or as hash in a :data key:
12
- # Campaign.create(:data => { :name => 'Campaign 123', :status => 'ENABLED' })
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
- # give users options to shorten input params
18
- params = { :data => params } unless params.has_key?(:data)
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
- # prepare for adding campaign
21
- ad_groups = params[:data].delete(:ad_groups).to_a
22
- targets = params[:data].delete(:targets)
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 => 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
- :data => ad_group_data.merge(:campaign_id => campaign[:id]),
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
- campaign
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 self.update(params = {})
60
- campaign_service = Campaign.new
114
+ def update(params = {})
115
+ # TODO validation or refuse to update
61
116
 
62
- # give users options to shorten input params
63
- params = { :data => params } unless params.has_key?(:data)
117
+ response = self.mutate(
118
+ :operator => 'SET',
119
+ :operand => params.merge(:id => @id)
120
+ )
64
121
 
65
- campaign_id = params[:id] || params[:data][:id] || nil
66
- return nil unless campaign_id
67
-
68
- operation = { :operator => 'SET',
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
- return campaign
127
+ true
82
128
  end
83
129
 
84
- def self.set_status(params = {})
85
- params[:id] ||= (params[:data] || params[:data][:id]) || nil
86
- return nil unless params[:id]
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
- self.update(:id => params[:id], :status => params[:status])
90
- end
134
+ def rename(new_name); update(:name => new_name); end
91
135
 
92
- def self.activate(params = {})
93
- self.set_status params.merge(:status => 'ACTIVE')
94
- end
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
- def self.pause(params = {})
97
- self.set_status params.merge(:status => 'PAUSED')
143
+ update(
144
+ :name => "#{@name}_DELETED_#{(Time.now.to_f * 1000).to_i}",
145
+ :status => 'DELETED'
146
+ )
98
147
  end
99
148
 
100
- def self.delete(params = {})
101
- self.set_status params.merge(:status => 'DELETED')
149
+ def find # == refresh
150
+ Campaign.find(:first, :id => @id)
102
151
  end
103
152
 
104
- def self.rename(params = {})
105
- params[:id] ||= (params[:data] || params[:data][:id]) || nil
106
- return nil unless (params[:id] && params[:name])
153
+ def self.find(amount = :all, params = {})
154
+ params.symbolize_keys!
155
+ first_only = (amount.to_sym == :first)
107
156
 
108
- self.update(:id => params[:id], :name => params[:name])
109
- end
157
+ raise "Campaign ID (:id param) is required" unless params[:id]
110
158
 
111
- def self.find(params = {})
112
- campaign_service = Campaign.new
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
- # :predicates => [{ :field => 'Id', :operator => 'EQUALS', :values => '334315' }]
117
- # :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}]
168
+ :fields => ['Id', 'Name', 'Status', 'BiddingStrategy' ],
169
+ :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
170
+ :predicates => predicates
118
171
  }
119
172
 
120
- # set filtering conditions: find by id, status etc.
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 = campaign_service.service.get(selector)
175
+ response = (response and response[:entries]) ? response[:entries] : []
128
176
 
129
- return (response and response[:entries]) ? response[:entries].to_a : []
130
-
131
- if response
132
- response[:entries].to_a.each do |campaign|
133
- puts "Campaign name is \"#{campaign[:name]}\", id is #{campaign[:id]} " +
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