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.
@@ -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