adapi 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,63 +4,66 @@ module Adapi
4
4
  #
5
5
  class CampaignTarget < Api
6
6
 
7
+ attr_accessor :campaign_id, :targets
8
+
9
+ validates_presence_of :campaign_id
10
+
11
+ # TODO validate if targets are in correct format
12
+
13
+ def attributes
14
+ super.merge( 'campaign_id' => campaign_id, 'targets' => targets )
15
+ end
16
+
7
17
  def initialize(params = {})
8
18
  params[:service_name] = :CampaignTargetService
9
- super(params)
10
- end
11
19
 
12
- # FIXME params should be the same as in other services, for example ad_group
13
- #
14
- def self.create(params = {})
15
- campaign_target_service = CampaignTarget.new
20
+ @xsi_type = 'CampaignTarget'
16
21
 
17
- raise "No Campaign ID" unless params[:campaign_id]
18
- campaign_id = params[:campaign_id].to_i
22
+ %w{ campaign_id targets }.each do |param_name|
23
+ self.send "#{param_name}=", params[param_name.to_sym]
24
+ end
19
25
 
26
+ super(params)
27
+ end
28
+
29
+ def set
20
30
  # transform our own high-level target parameters to google low-level
21
31
  # target parameters
22
32
  operations = []
23
33
 
24
- params[:targets].each_pair do |targetting_type, targetting_settings|
34
+ @targets.each_pair do |targetting_type, targetting_settings|
25
35
  operations << { :operator => 'SET',
26
36
  :operand => {
27
37
  :xsi_type => "#{targetting_type.to_s.capitalize}TargetList",
28
- :campaign_id => campaign_id,
29
- :targets => self.create_targets(targetting_type, targetting_settings)
38
+ :campaign_id => @campaign_id,
39
+ :targets => CampaignTarget::create_targets(targetting_type, targetting_settings)
30
40
  }
31
41
  }
32
42
  end
33
43
 
34
- response = campaign_target_service.service.mutate(operations)
44
+ response = self.mutate(operations)
35
45
 
36
- targets = response[:value] || []
37
- targets.each do |target|
38
- puts "Campaign target of type #{target[:"@xsi:type"]} for campaign id " +
39
- "#{target[:campaign_id]} was set."
40
- end
41
-
42
- targets
46
+ (response and response[:value]) ? true : false
43
47
  end
44
48
 
45
- def self.find(params = {})
46
- campaign_target_service = CampaignTarget.new
49
+ alias :create :set
50
+
51
+ # FIXME doesn't display everything, check the issues in google-adwords-api
52
+ #
53
+ def self.find(amount = :all, params = {})
54
+ params.symbolize_keys!
55
+ params = params[:conditions] if params[:conditions]
56
+ first_only = (amount.to_sym == :first)
47
57
 
48
- selector = {} # select all campaign targets by default
49
- selector[:campaign_ids] = params[:campaign_ids] if params[:campaign_ids]
58
+ raise ArgumentError, "Campaing ID is required" unless params[:campaign_id]
50
59
 
51
- response = campaign_target_service.service.get(selector)
52
-
53
- targets = nil
54
- if response and response[:entries]
55
- targets = response[:entries]
56
- targets.each do |target|
57
- p target
58
- end
59
- else
60
- puts "No campaign targets found."
61
- end
60
+ selector = { :campaign_ids => [ params[:campaign_id].to_i ] }
61
+
62
+ response = CampaignTarget.new.service.get(selector)
62
63
 
63
- targets
64
+ response = (response and response[:entries]) ? response[:entries] : []
65
+
66
+ first_only ? response.first : response
64
67
  end
65
68
 
66
69
  # transform our own high-level target parameters to google low-level
@@ -71,20 +74,65 @@ module Adapi
71
74
  target_data.map { |language| { :language_code => language } }
72
75
  # example: ['cz','sk'] => [{:language_code => 'cz'}, {:language_code => 'sk'}]
73
76
  when :geo
74
- geo_targets = []
75
- target_data.each_pair do |geo_type, geo_values|
76
- geo_values.each do |geo_value|
77
- geo_targets << {
78
- :xsi_type => "#{geo_type.to_s.capitalize}Target",
79
- :excluded => false,
80
- "#{geo_type}_code".to_sym => geo_value
81
- }
77
+ target_data.map do |geo_type, geo_values|
78
+ case geo_type
79
+ when :proximity
80
+ radius_in_units, radius_units = parse_radius(geo_values[:radius])
81
+ long, lat = parse_geodata(geo_values[:geo_point])
82
+
83
+ {
84
+ :xsi_type => "#{geo_type.to_s.capitalize}Target",
85
+ :excluded => false,
86
+ :radius_in_units => radius_in_units,
87
+ :radius_distance_units => radius_units,
88
+ :geo_point => {
89
+ :longitude_in_micro_degrees => long,
90
+ :latitude_in_micro_degrees => lat
91
+ }
92
+ }
93
+
94
+ when :city
95
+ geo_values.merge(
96
+ :xsi_type => "#{geo_type.to_s.capitalize}Target",
97
+ :excluded => false
98
+ )
99
+
100
+ else # :country, :province
101
+ {
102
+ :xsi_type => "#{geo_type.to_s.capitalize}Target",
103
+ :excluded => false,
104
+ "#{geo_type}_code".to_sym => geo_values
105
+ }
82
106
  end
83
107
  end
84
- geo_targets
85
- else nil
108
+ else nil
86
109
  end
87
110
  end
88
111
 
112
+ def self.parse_radius(radius)
113
+ radius_in_units, radius_units = radius.split(' ', 2)
114
+ [
115
+ radius_in_units.to_i,
116
+ (radius_units == 'm') ? 'MILES' : 'KILOMETERS'
117
+ ]
118
+ end
119
+
120
+ # parse longitude and lattitude from string in this format:
121
+ # "longitude,lattitude" to [int,int] in Google microdegrees
122
+ # for example: "38.89859,-77.035971" -> [38898590, -77035971]
123
+ #
124
+ def self.parse_geodata(long_lat)
125
+ long_lat.split(',', 2).map { |x| to_microdegrees(x) }
126
+ end
127
+
128
+ # convert latitude or longitude data to microdegrees,
129
+ # a format with AdWords API accepts
130
+ #
131
+ # TODO alias :to_microdegrees :to_micro_units
132
+ #
133
+ def self.to_microdegrees(x)
134
+ Api.to_micro_units(x)
135
+ end
136
+
89
137
  end
90
138
  end
@@ -18,9 +18,15 @@ module Adapi
18
18
  @data ||= self.settings[:default]
19
19
  end
20
20
 
21
- # TODO described in README, but should be documented here as well
21
+ # account_alias - alias of an account set in settings
22
+ # authentication_params - ...which we want to override
22
23
  #
23
- def self.set(params = {})
24
+ def self.set(account_alias = :default, authentication_params = {})
25
+ custom_settings = @settings[account_alias.to_sym]
26
+ custom_settings[:authentication] = custom_settings[:authentication].merge(authentication_params)
27
+ @data = custom_settings
28
+
29
+ =begin original method, to be merged into the new one
24
30
  # hash of params - default
25
31
  if params.is_a?(Hash)
26
32
  @data = params
@@ -28,6 +34,7 @@ module Adapi
28
34
  elsif params.is_a?(Symbol)
29
35
  @data = @settings[params]
30
36
  end
37
+ =end
31
38
  end
32
39
 
33
40
  # params:
@@ -37,6 +44,13 @@ module Adapi
37
44
  def self.load_settings(params = {})
38
45
  params[:path] ||= ENV['HOME']
39
46
  params[:filename] ||= 'adapi.yml'
47
+ params[:in_hash] ||= nil
48
+
49
+ # HOTFIX enable load by hash
50
+ if params[:in_hash]
51
+ @settings = params[:in_hash]
52
+ return @settings
53
+ end
40
54
 
41
55
  adapi_path = File.join(params[:path], params[:filename])
42
56
  adwords_api_path = File.join(ENV['HOME'], 'adwords_api.yml')
@@ -0,0 +1,109 @@
1
+ module Adapi
2
+ class Keyword < AdGroupCriterion
3
+
4
+ attr_accessor :keywords
5
+
6
+ def attributes
7
+ super.merge('keywords' => keywords)
8
+ end
9
+
10
+ def initialize(params = {})
11
+ params[:service_name] = :AdGroupCriterionService
12
+
13
+ @xsi_type = 'AdGroupCriterion'
14
+
15
+ %w{ keywords }.each do |param_name|
16
+ self.send "#{param_name}=", params[param_name.to_sym]
17
+ end
18
+
19
+ self.keywords ||= []
20
+ self.keywords.map! { |k| Keyword.keyword_attributes(k) }
21
+
22
+ super(params)
23
+ end
24
+
25
+ # TODO include formatting in create method
26
+ #
27
+ def self.keyword_attributes(keyword)
28
+ # detect match type
29
+ match_type = case keyword[0]
30
+ when '"'
31
+ keyword = keyword.slice(1, (keyword.size - 2))
32
+ 'PHRASE'
33
+ when '['
34
+ keyword = keyword.slice(1, (keyword.size - 2))
35
+ 'EXACT'
36
+ else
37
+ 'BROAD'
38
+ end
39
+
40
+ # detect if keyword is negative
41
+ negative = if (keyword =~ /^\-/)
42
+ keyword.slice!(0, 1)
43
+ true
44
+ else
45
+ false
46
+ end
47
+
48
+ { :text => keyword, :match_type => match_type, :negative => negative }
49
+ end
50
+
51
+ def create
52
+ operations = @keywords.map do |keyword|
53
+ {
54
+ :operator => 'ADD',
55
+ :operand => {
56
+ :xsi_type => (keyword[:negative] ? 'NegativeAdGroupCriterion' : 'BiddableAdGroupCriterion'),
57
+ :ad_group_id => @ad_group_id,
58
+ :criterion => {
59
+ :xsi_type => 'Keyword',
60
+ :text => keyword[:text],
61
+ :match_type => keyword[:match_type]
62
+ }
63
+ }
64
+ }
65
+ end
66
+
67
+ response = self.mutate(operations)
68
+
69
+ return false unless (response and response[:value])
70
+
71
+ self.keywords = response[:value].map { |keyword| keyword[:criterion] }
72
+
73
+ true
74
+ end
75
+
76
+ def self.find(amount = :all, params = {})
77
+ params.symbolize_keys!
78
+ # this has no effect, it's here just to have the same interface everywhere
79
+ first_only = (amount.to_sym == :first)
80
+
81
+ # we need ad_group_id
82
+ raise ArgumentError, "AdGroup ID is required" unless params[:ad_group_id]
83
+
84
+ # supported condition parameters: ad_group_id and id
85
+ predicates = [ :ad_group_id ].map do |param_name|
86
+ if params[param_name]
87
+ {:field => param_name.to_s.camelcase, :operator => 'EQUALS', :values => params[param_name] }
88
+ end
89
+ end.compact
90
+
91
+ # Get all the criteria for this ad group.
92
+ selector = {
93
+ :fields => ['Id', 'Text'],
94
+ :ordering => [{ :field => 'AdGroupId', :sort_order => 'ASCENDING' }],
95
+ :predicates => predicates
96
+ }
97
+
98
+ response = Keyword.new.service.get(selector)
99
+
100
+ response = (response and response[:entries]) ? response[:entries] : []
101
+
102
+ Keyword.new(
103
+ :ad_group_id => params[:ad_group_id],
104
+ :keywords => response.map { |keyword| keyword[:criterion] }
105
+ )
106
+ end
107
+
108
+ end
109
+ end
@@ -1,7 +1,18 @@
1
1
  module Adapi
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
 
4
4
  # CHANGELOG:
5
+ #
6
+ # 0.0.3
7
+ # converted to ActiveModel
8
+ # moved common functionality to Api class
9
+ # changed http client to curb and hotfix ssl authentication bug in HTTPI
10
+ # added basic error handling
11
+ # changed DSL for Campaign attributes
12
+ # changed Ad model to general Ad model and moved TextAd to separate model
13
+ # added support for more target types and changed DSL for CampaignTarget
14
+ # converted to Ruby 1.9.2 (should work in Ruby 1.8.7 as well)
15
+ #
5
16
  # 0.0.2
6
17
  # [FIX] switched google gem dependencies from edge to stable release
7
18
  end
@@ -0,0 +1,35 @@
1
+
2
+ # manually hardcode timeouts for HTTPI to 5 minutes (300 seconds)
3
+ # HOTFIX there's no way how to do it properly through HTTPI
4
+
5
+ module HTTPI
6
+ class Request
7
+ def open_timeout
8
+ 300
9
+ end
10
+
11
+ def read_timeout
12
+ 300
13
+ end
14
+ end
15
+ end
16
+
17
+ # disable ssl authentication in curb
18
+ # HOTFIX for bug in HTTPI
19
+
20
+ module HTTPI
21
+ module Adapter
22
+ class Curb
23
+
24
+ private
25
+
26
+ def setup_client(request)
27
+ basic_setup request
28
+ setup_http_auth request if request.auth.http?
29
+ # setup_ssl_auth request.auth.ssl if request.auth.ssl?
30
+ # setup_ntlm_auth request if request.auth.ntlm?
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+
2
+ Factory.define :ad_group, :class => Adapi::AdGroup do |f|
3
+ f.sequence(:campaign_id) { |n| n }
4
+ f.name "AdGroup %d" % (Time.new.to_f * 1000).to_i
5
+ f.status 'ENABLED'
6
+ # f.bids {}
7
+ f.keywords [ 'dem codez', '"top coder"', '[-code]' ]
8
+ end
9
+
10
+ =begin
11
+ ad_group_data = {
12
+ :bids => {
13
+ :xsi_type => 'ManualCPCAdGroupBids',
14
+ :keyword_max_cpc => {
15
+ :amount => {
16
+ :micro_amount => 10000000
17
+ }
18
+ }
19
+ },
20
+ =end
@@ -0,0 +1,9 @@
1
+
2
+ Factory.define :text_ad, :class => Adapi::Ad::TextAd do |f|
3
+ f.sequence(:ad_group_id) { |n| n }
4
+ f.headline 'Code like Neo'
5
+ f.description1 'Need mad coding skills?'
6
+ f.description2 'Check out my new blog!'
7
+ f.url 'http://www.demcodez.com'
8
+ f.display_url 'http://www.demcodez.com'
9
+ end
@@ -8,13 +8,13 @@ require 'factory_girl'
8
8
 
9
9
  # always test the latest version of the gem
10
10
  # TODO make it an option only through ENV variable switch
11
- require 'lib/adapi'
11
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'adapi')
12
12
 
13
13
  # load factories
14
- Dir[ File.join(File.dirname(__FILE__), 'factories/*.rb') ].each { |f| require f }
14
+ Dir[ File.join(File.dirname(__FILE__), 'factories', '*.rb') ].each { |f| require f }
15
15
 
16
16
  class Test::Unit::TestCase
17
-
17
+
18
18
  FakeWeb.allow_net_connect = false
19
19
 
20
20
  end
@@ -0,0 +1,30 @@
1
+ require 'test_helper'
2
+
3
+ module Adapi
4
+ class TextAdTest < Test::Unit::TestCase
5
+ include ActiveModel::Lint::Tests
6
+
7
+ def setup
8
+ @model = Ad::TextAd.new
9
+ end
10
+
11
+ context "valid new TextAd instance" do
12
+ setup do
13
+ @text_ad = Factory.build(:text_ad)
14
+ end
15
+
16
+ should "be valid" do
17
+ assert @text_ad.valid?
18
+ end
19
+
20
+ context " / data method" do
21
+ should "return TextAd params in hash" do
22
+ assert @text_ad.data.is_a?(Hash)
23
+ assert_equal @text_ad.headline, @text_ad.data[:headline]
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end