adapi 0.0.8 → 0.0.9

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