adapi 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +1 -1
  2. data/README.markdown +2 -1
  3. data/adapi.gemspec +11 -11
  4. data/examples/add_ad_group.rb +0 -0
  5. data/examples/add_bare_ad_group.rb +3 -1
  6. data/examples/add_bare_campaign.rb +6 -4
  7. data/examples/add_campaign.rb +10 -3
  8. data/examples/add_campaign_criteria.rb +5 -5
  9. data/examples/add_invalid_ad_group.rb +4 -6
  10. data/examples/add_invalid_keywords.rb +40 -0
  11. data/examples/add_invalid_text_ad.rb +6 -6
  12. data/examples/add_keywords.rb +0 -0
  13. data/examples/add_negative_campaign_criteria.rb +0 -0
  14. data/examples/add_text_ad.rb +5 -9
  15. data/examples/custom_settings.yml +8 -0
  16. data/examples/customize_configuration.rb +0 -0
  17. data/examples/delete_ad_group.rb +11 -0
  18. data/examples/delete_keyword.rb +0 -0
  19. data/examples/delete_text_ad.rb +13 -0
  20. data/examples/find_ad_group.rb +10 -0
  21. data/examples/find_all_campaigns.rb +0 -0
  22. data/examples/find_bare_campaign.rb +34 -0
  23. data/examples/find_campaign.rb +0 -0
  24. data/examples/find_campaign_ad_groups.rb +0 -0
  25. data/examples/find_campaign_criteria.rb +0 -0
  26. data/examples/find_locations.rb +0 -0
  27. data/examples/log_to_specific_account.rb +0 -0
  28. data/examples/rollback_campaign.rb +0 -0
  29. data/examples/test_diacritics.rb +0 -0
  30. data/examples/update_ad_group.rb +70 -0
  31. data/examples/update_campaign.rb +34 -9
  32. data/examples/update_campaign_ad_groups.rb +137 -0
  33. data/examples/update_campaign_criteria.rb +33 -0
  34. data/examples/update_complete_campaign.rb +177 -0
  35. data/examples/update_text_ad.rb +18 -0
  36. data/lib/adapi/ad/text_ad.rb +39 -49
  37. data/lib/adapi/ad.rb +30 -19
  38. data/lib/adapi/ad_group.rb +110 -30
  39. data/lib/adapi/api.rb +25 -28
  40. data/lib/adapi/campaign.rb +216 -70
  41. data/lib/adapi/campaign_criterion.rb +44 -5
  42. data/lib/adapi/config.rb +1 -4
  43. data/lib/adapi/constant_data/language.rb +11 -2
  44. data/lib/adapi/keyword.rb +60 -1
  45. data/lib/adapi/version.rb +11 -2
  46. data/lib/adapi.rb +10 -7
  47. data/test/{config/adapi.yml.template → fixtures/adapi.yml} +11 -2
  48. data/test/{config/adwords_api.yml.template → fixtures/adwords_api.yml} +6 -2
  49. data/test/integration/find_location_test.rb +1 -1
  50. data/test/unit/ad_group_test.rb +2 -2
  51. data/test/unit/config_test.rb +8 -27
  52. data/test/unit/constant_data/language_test.rb +34 -0
  53. metadata +52 -108
  54. data/examples/update_campaign_status.rb +0 -15
  55. data/lib/savon_monkeypatch.rb +0 -43
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+
3
+ require 'adapi'
4
+
5
+ require_relative 'add_campaign_criteria'
6
+
7
+ =begin this is an obsolete way to do this
8
+ Adapi::CampaignCriterion.new(
9
+ :campaign_id => $campaign[:id],
10
+ :criteria => {
11
+ :language => %w{ en cs},
12
+ }
13
+ ).destroy
14
+
15
+ Adapi::CampaignCriterion.new(
16
+ :campaign_id => $campaign[:id],
17
+ :criteria => {
18
+ :language => %w{ sk },
19
+ }
20
+ ).create
21
+ =end
22
+
23
+ $new_criteria = Adapi::CampaignCriterion.new(
24
+ :campaign_id => $campaign[:id],
25
+ :criteria => {
26
+ :language => %w{ sk },
27
+ }
28
+ )
29
+
30
+ $result = $new_criteria.update!
31
+
32
+ $campaign_criterion = Adapi::CampaignCriterion.find( :campaign_id => $campaign[:id] )
33
+ pp $campaign_criterion
@@ -0,0 +1,177 @@
1
+ # encoding: utf-8
2
+
3
+ require 'adapi'
4
+
5
+ # create campaign by single command, with campaing targets, with ad_groups
6
+ # including keywords and ads
7
+
8
+ $ad_group_names = [
9
+ "AdGroup 01 #%d" % (Time.new.to_f * 1000).to_i,
10
+ "AdGroup 02 #%d" % (Time.new.to_f * 1000).to_i
11
+ ]
12
+
13
+ campaign_data = {
14
+ # basic data for campaign
15
+ :name => "Campaign #%d" % (Time.new.to_f * 1000).to_i,
16
+ :status => 'PAUSED',
17
+ :bidding_strategy => 'ManualCPC',
18
+ :budget => 50,
19
+ :network_setting => {
20
+ :target_google_search => true,
21
+ :target_search_network => true,
22
+ :target_content_network => false,
23
+ :target_content_contextual => false
24
+ },
25
+
26
+ :criteria => {
27
+ :language => [ :en, :cs ],
28
+ :geo => { :proximity => { :geo_point => '38.89859,-77.035971', :radius => '10 km' } }
29
+ },
30
+
31
+ :ad_groups => [
32
+ {
33
+ :name => $ad_group_names[0],
34
+ :status => 'ENABLED',
35
+
36
+ :keywords => [ 'neo', 'dem codez', '"top coder"', "[-code]" ],
37
+
38
+ :ads => [
39
+ {
40
+ :headline => "Code like Neo",
41
+ :description1 => 'Need mad coding skills?',
42
+ :description2 => 'Check out my new blog!',
43
+ :url => 'http://www.demcodez.com',
44
+ :display_url => 'http://www.demcodez.com'
45
+ }
46
+ ]
47
+ },
48
+
49
+ {
50
+ :name => $ad_group_names[1],
51
+ :status => 'PAUSED',
52
+
53
+ :keywords => [ 'dem codez', 'trinity', 'morpheus', '"top coder"', "[-code]" ],
54
+
55
+ :ads => [
56
+ {
57
+ :headline => "Code like Trinity",
58
+ :description1 => 'The power of awesomeness?',
59
+ :description2 => 'Check out my new blog!',
60
+ :url => 'http://www.demcodez.com',
61
+ :display_url => 'http://www.demcodez.com'
62
+ },
63
+
64
+ {
65
+ :headline => "Code like Morpheus",
66
+ :description1 => 'Unleash the power of Matrix',
67
+ :description2 => 'Check out my new blog',
68
+ :url => 'http://www.demcodez.com',
69
+ :display_url => 'http://www.demcodez.com'
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+
75
+ }
76
+
77
+ $campaign = Adapi::Campaign.create(campaign_data)
78
+ p "Created campaign ID #{$campaign.id}"
79
+
80
+ # PS: changes in ad_groups:
81
+ # * delete first ad_group
82
+ # * change second ad_group
83
+ # * add new ad_group
84
+
85
+ Adapi::Campaign.update(
86
+ :id => $campaign[:id],
87
+ :status => 'ACTIVE',
88
+ :name => "UPDATED #{$campaign[:name]}",
89
+ :bidding_strategy => {
90
+ :xsi_type => 'BudgetOptimizer',
91
+ :bid_ceiling => 20
92
+ },
93
+ :budget => 75,
94
+
95
+ # deletes all criteria (except :platform) and create these new ones
96
+ :criteria => {
97
+ :language => [ :sk ],
98
+ },
99
+
100
+ :ad_groups => [
101
+ # no match here for $ad_group_names[0], so it's going to be deleted
102
+
103
+ # this ad_group will be created
104
+ {
105
+ :name => "UPDATED " + $ad_group_names[0],
106
+ :status => 'ENABLED',
107
+
108
+ :keywords => [ 'neo update', 'dem codezzz', '"top coder"' ],
109
+
110
+ :ads => [
111
+ {
112
+ :headline => "Update like Neo",
113
+ :description1 => 'Need mad coding skills?',
114
+ :description2 => 'Check out my new blog!',
115
+ :url => 'http://www.demcodez.com',
116
+ :display_url => 'http://www.demcodez.com'
117
+ }
118
+ ]
119
+ },
120
+
121
+ # this ad_group is going to be updated
122
+ {
123
+ :name => $ad_group_names[1],
124
+ :status => 'ENABLED', # from PAUSED
125
+
126
+ :keywords => [ 'dem updatez', 'update trinity', 'update morpheus' ],
127
+
128
+ :ads => [
129
+ {
130
+ :headline => "Update like Trinity",
131
+ :description1 => 'The power of updates?',
132
+ :description2 => 'Check out my new blog!',
133
+ :url => 'http://www.demcodez.com',
134
+ :display_url => 'http://www.demcodez.com'
135
+ },
136
+
137
+ {
138
+ :headline => "Update like Morpheus",
139
+ :description1 => 'Unleash the power of updates',
140
+ :description2 => 'Check out my new blog',
141
+ :url => 'http://www.demcodez.com',
142
+ :display_url => 'http://www.demcodez.com'
143
+ }
144
+ ]
145
+ }
146
+ ]
147
+ )
148
+
149
+ unless $campaign.errors.empty?
150
+
151
+ puts "ERROR WHEN UPDATING AD GROUPS:"
152
+ puts $campaign.errors.full_messages.join("\n")
153
+
154
+ else
155
+
156
+ # reload campaign
157
+ $campaign = Adapi::Campaign.find_complete($campaign.id)
158
+
159
+ $campaign_attributes = $campaign.attributes
160
+ $criteria = $campaign_attributes.delete(:criteria)
161
+ $ad_groups = $campaign_attributes.delete(:ad_groups)
162
+
163
+ puts "\nCAMPAIGN UPDATED\n"
164
+
165
+ puts "\nCAMPAIGN DATA:"
166
+ pp $campaign_attributes
167
+
168
+ puts "\nCAMPAIGN CRITERIA:"
169
+ pp $criteria
170
+
171
+ puts "\nAD GROUPS (#{$ad_groups.size}):"
172
+ $ad_groups.each_with_index do |ad_group, i|
173
+ puts "\nAD GROUP #{i + 1}:\n"
174
+ pp ad_group.attributes
175
+ end
176
+
177
+ end
@@ -0,0 +1,18 @@
1
+
2
+ require 'adapi'
3
+
4
+ require_relative 'add_text_ad'
5
+
6
+ $new_ad = $ad.update(
7
+ :status => 'ACTIVE',
8
+ :headline => "Code like Trinity",
9
+ :description1 => 'Need mad update skills?',
10
+ :description2 => 'Check out my updates!',
11
+ :url => 'http://www.demupdatez.com',
12
+ :display_url => 'http://www.demupdatez.com'
13
+ )
14
+
15
+ $new_ad = Adapi::Ad::TextAd.find(:first, :ad_group_id => $new_ad.ad_group_id, :id => $new_ad.id )
16
+
17
+ puts "\nUPDATED (AND RELOADED) AD:"
18
+ pp $new_ad.attributes
@@ -7,19 +7,23 @@ module Adapi
7
7
  #
8
8
  class Ad::TextAd < Ad
9
9
 
10
- attr_accessor :headline, :description1, :description2
10
+ ATTRIBUTES = [ :headline, :description1, :description2 ]
11
+
12
+ attr_accessor *ATTRIBUTES
11
13
 
12
14
  def attributes
13
- super.merge('headline' => headline, 'description1' => description1, 'description2' => description2)
15
+ super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
14
16
  end
15
17
 
18
+ alias to_hash attributes
19
+
16
20
  def initialize(params = {})
17
21
  params[:service_name] = :AdGroupAdService
18
22
 
19
23
  @xsi_type = 'TextAd'
20
24
 
21
- %w{ headline description1 description2 }.each do |param_name|
22
- self.send "#{param_name}=", params[param_name.to_sym]
25
+ ATTRIBUTES.each do |param_name|
26
+ self.send("#{param_name}=", params[param_name])
23
27
  end
24
28
 
25
29
  super(params)
@@ -30,57 +34,58 @@ module Adapi
30
34
  end
31
35
 
32
36
  def create
37
+ operand = self.attributes.delete_if do |k|
38
+ [ :campaign_id, :ad_group_id, :id, :status ].include?(k.to_sym)
39
+ end.symbolize_keys
40
+
33
41
  operation = {
34
42
  :operator => 'ADD',
35
43
  :operand => {
36
44
  :ad_group_id => @ad_group_id,
37
45
  :status => @status,
38
- :ad => self.data
46
+ :ad => operand
39
47
  }
40
48
  }
41
49
 
42
50
  response = self.mutate(operation)
43
-
44
- # check for PolicyViolationError(s)
45
- # PS: check google-adwords-api/examples/handle_policy_violation_error.rb
46
- if (self.errors['PolicyViolationError'].size > 0)
47
- # set exemptions and try again
48
- operation[:exemption_requests] = errors['PolicyViolationError'].map do |error|
49
- { :key => error }
51
+
52
+ # check for PolicyViolationErrors, set exemptions and try again
53
+ # TODO for now, this is only done once. how about setting a number of retries?
54
+ unless self.errors[:PolicyViolationError].empty?
55
+ operation[:exemption_requests] = self.errors[:PolicyViolationError].map do |error_key|
56
+ { :key => error_key }
50
57
  end
51
58
 
52
59
  self.errors.clear
53
60
 
54
- response = self.mutate(operation)
61
+ response = self.mutate(operation)
55
62
  end
56
63
 
57
- return false unless (response and response[:value])
58
-
64
+ return false unless self.errors.empty?
65
+
66
+ # set ad id
59
67
  self.id = response[:value].first[:ad][:id] rescue nil
60
68
 
61
69
  true
62
70
  end
63
71
 
64
- # params - specify hash of params and values to update
65
- # PS: I think it's possible to edit only status, but not headline,
66
- # descriptions... instead you should delete existing ad and create a new one
72
+ # except for status, we cannot edit ad fields
73
+ # gotta delete an ad and create a new one instead
74
+ # this means that this method returns new ad id!
75
+ #
76
+ # REFACTOR this method shpould be removed (but that might break something,
77
+ # os let's keep it here for the moment)
67
78
  #
68
79
  def update(params = {})
69
- # set params (:status param makes it a little complicated)
70
- #
71
- updated_params = (params || self.attributes).symbolize_keys
72
- updated_status = updated_params.delete(:status)
73
-
74
- response = self.mutate(
75
- :operator => 'SET',
76
- :operand => {
77
- :ad_group_id => self.ad_group_id,
78
- :ad => updated_params.merge(:id => self.id),
79
- :status => updated_status
80
- }
81
- )
80
+ # set attributes for the "updated" ad
81
+ create_attributes = self.attributes.merge(params).symbolize_keys
82
+ create_attributes.delete(:id)
83
+
84
+ # delete current ad
85
+ return false unless self.destroy
82
86
 
83
- (response and response[:value]) ? true : false
87
+ # create new add
88
+ TextAd.create(create_attributes)
84
89
  end
85
90
 
86
91
  def find # == refresh
@@ -101,13 +106,12 @@ module Adapi
101
106
  # supported condition parameters: ad_group_id and id
102
107
  predicates = [ :ad_group_id, :id ].map do |param_name|
103
108
  if params[param_name]
104
- value = Array.try_convert(params[param_name]) ? params_param_name : [params[param_name]]
105
- {:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
109
+ { :field => param_name.to_s.camelcase, :operator => 'IN', :values => Array( params[param_name] ) }
106
110
  end
107
111
  end.compact
108
112
 
109
113
  selector = {
110
- :fields => ['Id', 'Headline'],
114
+ :fields => ['Id', 'AdGroupId', 'Headline' ],
111
115
  :ordering => [{:field => 'Id', :sort_order => 'ASCENDING'}],
112
116
  :predicates => predicates
113
117
  }
@@ -121,22 +125,8 @@ module Adapi
121
125
  end
122
126
 
123
127
  # TODO convert to TextAd instances
124
- # PS: we already have ad_group_id parameter
125
128
  first_only ? response.first : response
126
129
  end
127
130
 
128
- # Converts text ad data to hash - of the same structure which is used when
129
- # creating a complete campaign.
130
- #
131
- def to_hash
132
- {
133
- :headline => self[:headline],
134
- :description1 => self[:description1],
135
- :description2 => self[:description2],
136
- :url => self[:url],
137
- :display_url => self[:display_url]
138
- }
139
- end
140
-
141
131
  end
142
132
  end
data/lib/adapi/ad.rb CHANGED
@@ -6,7 +6,9 @@ module Adapi
6
6
  # wraps all types of ads: text ads, image ads...
7
7
  class Ad < Api
8
8
 
9
- attr_accessor :ad_group_id, :url, :display_url, :approval_status,
9
+ # REFACTOR attributes
10
+
11
+ attr_accessor :id, :ad_group_id, :url, :display_url, :approval_status,
10
12
  :disapproval_reasons, :trademark_disapproved
11
13
 
12
14
  validates_presence_of :ad_group_id
@@ -14,7 +16,8 @@ module Adapi
14
16
  # PS: create won't work with id and ad_group_id
15
17
  # 'id' => id, 'ad_group_id' => ad_group_id,
16
18
  def attributes
17
- super.merge('url' => url, 'display_url' => display_url)
19
+ super.merge( 'id' => id, 'ad_group_id' => ad_group_id,
20
+ 'url' => url, 'display_url' => display_url )
18
21
  end
19
22
 
20
23
  def initialize(params = {})
@@ -33,15 +36,14 @@ module Adapi
33
36
  response = self.mutate(
34
37
  :operator => 'REMOVE',
35
38
  :operand => {
36
- :ad_group_id => self.ad_group_id,
37
- :ad => { :id => self.id, :xsi_type => 'Ad' }
39
+ :ad_group_id => @ad_group_id,
40
+ :ad => { :id => @id, :xsi_type => 'Ad' }
38
41
  }
39
42
  )
40
43
 
41
44
  (response and response[:value]) ? true : false
42
45
  end
43
46
 
44
-
45
47
  # ad-specific mutate wrapper, deals with PolicyViolations for ads
46
48
  #
47
49
  def mutate(operation)
@@ -53,25 +55,34 @@ module Adapi
53
55
  op
54
56
  end
55
57
 
56
- begin
58
+ begin
59
+
57
60
  response = @service.mutate(operation)
58
-
59
- rescue AdsCommon::Errors::HttpError => e
60
- self.errors.add(:base, e.message)
61
61
 
62
- # traps any exceptions raised by AdWords API
63
- rescue AdwordsApi::Errors::ApiException => e
64
- # return PolicyViolations so they can be sent again
62
+ rescue *API_EXCEPTIONS => e
63
+
64
+ # return PolicyViolations in specific format so they can be sent again
65
+ # see adwords-api gem example for details: handle_policy_violation_error.rb
65
66
  e.errors.each do |error|
66
- if (error[:api_error_type] == 'PolicyViolationError') && error[:is_exemptable]
67
- self.errors.add(error[:api_error_type], error[:key])
68
- else
69
- # otherwise, just report the errors
70
- self.errors.add( "[#{self.xsi_type.underscore}]", "#{error[:error_string]} @ #{error[:field_path]}")
67
+ # error[:xsi_type] seems to be broken, so using also alternative key
68
+ # also could try: :"@xsi:type" (but api_error_type seems to be more robust)
69
+ if (error[:xsi_type] == 'PolicyViolationError') || (error[:api_error_type] == 'PolicyViolationError')
70
+ if error[:is_exemptable]
71
+ self.errors.add(:PolicyViolationError, error[:key])
72
+ end
73
+
74
+ # return also exemptable errors, operation may fail even with them
75
+ self.errors.add(:base, "violated %s policy: \"%s\" on \"%s\"" % [
76
+ error[:is_exemptable] ? 'exemptable' : 'non-exemptable',
77
+ error[:key][:policy_name],
78
+ error[:key][:violating_text]
79
+ ])
80
+ else
81
+ self.errors.add(:base, e.message)
71
82
  end
72
- end
83
+ end # of errors.each
73
84
  end
74
-
85
+
75
86
  response
76
87
  end
77
88
 
@@ -1,31 +1,49 @@
1
1
  # encoding: utf-8
2
2
 
3
+ # This class handles operations with ad_groups
4
+ #
5
+ # https://developers.google.com/adwords/api/docs/reference/latest/AdGroupService
6
+ #
3
7
  module Adapi
4
8
  class AdGroup < Api
5
9
 
6
- attr_accessor :campaign_id, :name, :bids, :keywords, :ads
10
+ ATTRIBUTES = [ :id, :campaign_id, :name, :status, :bids, :keywords, :ads ]
11
+
12
+ attr_accessor *ATTRIBUTES
7
13
 
8
14
  validates_presence_of :campaign_id, :name, :status
9
15
  validates_inclusion_of :status, :in => %w{ ENABLED PAUSED DELETED }
10
16
 
11
17
  def attributes
12
- super.merge('campaign_id' => campaign_id, 'name' => name, 'bids' => bids)
18
+ super.merge Hash[ ATTRIBUTES.map { |k| [k, self.send(k)] } ]
13
19
  end
14
20
 
21
+ alias to_hash attributes
22
+
15
23
  def initialize(params = {})
16
24
  params[:service_name] = :AdGroupService
17
25
 
18
26
  @xsi_type = 'AdGroup'
19
27
 
20
- %w{ campaign_id name status bids keywords ads }.each do |param_name|
21
- self.send "#{param_name}=", params[param_name.to_sym]
28
+ ATTRIBUTES.each do |param_name|
29
+ self.send("#{param_name}=", params[param_name])
22
30
  end
23
31
 
24
- # convert bids to GoogleApi format
25
- #
26
- # can be either string (just xsi_type) or hash (xsi_type with params)
27
- # although I'm not sure if just string makes sense in this case
28
- #
32
+ @keywords ||= []
33
+
34
+ @ads ||= []
35
+
36
+ super(params)
37
+ end
38
+
39
+ # convert bids to GoogleApi format
40
+ #
41
+ # can be either string (just xsi_type) or hash (xsi_type with params)
42
+ # although I'm not sure if just string makes sense in this case
43
+ #
44
+ def bids=(params = {})
45
+ @bids = params
46
+
29
47
  if @bids
30
48
  unless @bids.is_a?(Hash)
31
49
  @bids = { :xsi_type => @bids }
@@ -42,11 +60,6 @@ module Adapi
42
60
  end
43
61
  end
44
62
  end
45
-
46
- @keywords ||= []
47
- @ads ||= []
48
-
49
- super(params)
50
63
  end
51
64
 
52
65
  def create
@@ -90,22 +103,98 @@ module Adapi
90
103
 
91
104
  true
92
105
  end
106
+
107
+ def update(params = {})
108
+ # step 1. update core attributes
109
+ core_attributes = [ :id, :campaign_id, :name, :status, :bids ]
110
+ # get operand in google format
111
+ # parse the given params by initialize method...
112
+ ad_group = Adapi::AdGroup.new(params)
113
+ # HOTFIX remove :service_name param inserted by initialize method
114
+ params.delete(:service_name)
115
+ # ...and load parsed params back into the hash
116
+ core_params = Hash[ core_attributes.map { |k| [k, ad_group.send(k)] if params[k].present? }.compact ]
117
+
118
+ response = ad_group.mutate(
119
+ :operator => 'SET',
120
+ :operand => core_params.merge( :id => @id, :campaign_id => @campaign_id )
121
+ )
122
+
123
+ return false unless (response and response[:value])
124
+
125
+ # step 2. update keywords
126
+ # delete everything and create new keywords
127
+ if params[:keywords] and not params[:keywords].empty?
128
+ # delete existing keywords
129
+ # OPTIMIZE should be all in one request
130
+ Keyword.find(:all, :ad_group_id => @id).keywords.each do |keyword|
131
+ Keyword.new(:ad_group_id => @id).delete(keyword[:text][:criterion][:id])
132
+ end
133
+
134
+ # create new keywords
135
+ result = Adapi::Keyword.create(
136
+ :ad_group_id => @id,
137
+ :keywords => params[:keywords]
138
+ )
139
+
140
+ unless result.errors.empty?
141
+ self.errors.add("Keyword", result.errors.to_a)
142
+ return false
143
+ end
144
+ end
145
+
146
+ # step 3. update ads
147
+ # ads can't be updated, gotta remove them all and add new ads
148
+ if params[:ads] and not params[:ads].empty?
149
+ # remove all existing ads
150
+ self.find_ads.each do |ad|
151
+ unless ad.destroy
152
+ self.errors.add("Ad \"#{ad.headline}\"", ["cannot be deleted"])
153
+ return false
154
+ end
155
+ end
156
+
157
+ # create new ads
158
+ params[:ads].each do |ad|
159
+ ad = Adapi::Ad::TextAd.create( ad.merge(:ad_group_id => @id) )
160
+
161
+ unless ad.errors.empty?
162
+ self.errors.add("Ad \"#{ad.headline}\"", ad.errors.to_a)
163
+ return false
164
+ end
165
+ end
166
+ end
167
+
168
+ true
169
+ end
93
170
 
171
+ # PS: perhaps also change the ad_group name when deleting
172
+ def delete
173
+ update(:status => 'DELETED')
174
+ end
175
+
94
176
  def self.find(amount = :all, params = {})
95
177
  params.symbolize_keys!
96
178
  first_only = (amount.to_sym == :first)
179
+ # by default, exclude ad_groups with status DELETED
180
+ params[:status] ||= %w{ ENABLED PAUSED }
97
181
 
98
182
  raise "Campaign ID is required" unless params[:campaign_id]
99
183
 
100
- predicates = [ :campaign_id, :id ].map do |param_name|
101
- if params[param_name]
102
- value = Array.try_convert(params[param_name]) ? params_param_name : [params[param_name]]
103
- {:field => param_name.to_s.camelcase, :operator => 'IN', :values => value }
184
+ predicates = [ :campaign_id, :id, :name, :status ].map do |param_name|
185
+ if params[param_name].present?
186
+ {:field => param_name.to_s.camelcase, :operator => 'IN', :values => Array( params[param_name] ) }
104
187
  end
105
188
  end.compact
106
189
 
190
+ select_fields = %w{ Id CampaignId Name Status }
191
+ # add Bids atributes
192
+ select_fields += %w{ EnhancedCpcEnabled
193
+ ProxyKeywordMaxCpc ProxySiteMaxCpc
194
+ KeywordMaxCpc KeywordContentMaxCpc }
195
+
107
196
  selector = {
108
- :fields => ['Id', 'Name', 'Status'],
197
+ :fields => select_fields,
109
198
  :ordering => [{:field => 'Name', :sort_order => 'ASCENDING'}],
110
199
  :predicates => predicates
111
200
  }
@@ -124,6 +213,8 @@ module Adapi
124
213
  )
125
214
  end
126
215
 
216
+ ad_groups.map! { |ad_group| AdGroup.new(ad_group) }
217
+
127
218
  first_only ? ad_groups.first : ad_groups
128
219
  end
129
220
 
@@ -136,16 +227,5 @@ module Adapi
136
227
  Ad::TextAd.find( (first_only ? :first : :all), :ad_group_id => self.id )
137
228
  end
138
229
 
139
- # Converts ad group data to hash - of the same structure which is used when
140
- # creating an ad group.
141
- #
142
- def to_hash
143
- ad_group_hash = {
144
- :id => self[:id],
145
- :name => self[:name],
146
- :status => self[:status]
147
- }
148
- end
149
-
150
230
  end
151
231
  end