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