From be5caf4c7ccb08f4f4f6c9f3c870996fd42235e9 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 02:06:45 -0500 Subject: [PATCH 01/31] a few more tests and have cm object types inherit from CM::Base so that the client instance is passed around and we only need to set the API_KEY once and it doesn't have to be a global --- lib/campaign_monitor.rb | 9 +++-- lib/campaign_monitor/base.rb | 21 ++++++++++ lib/campaign_monitor/client.rb | 4 +- lib/campaign_monitor/list.rb | 4 +- lib/campaign_monitor/subscriber.rb | 4 +- test/campaign_monitor_test.rb | 62 +++++++++++++++++++++++------- 6 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 lib/campaign_monitor/base.rb diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 850742e..893eee3 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -64,6 +64,7 @@ require 'date' require File.join(File.dirname(__FILE__), 'campaign_monitor/helpers.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/base.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/client.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/list.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/subscriber.rb') @@ -79,13 +80,14 @@ class CampaignMonitor def initialize(api_key=CAMPAIGN_MONITOR_API_KEY) @api_key = api_key @api_url = 'http://api.createsend.com/api/api.asmx' + CampaignMonitor::Base.client=self end - # Takes a CampaignMonitor API method name and set of parameters; # returns an XmlSimple object with the response def request(method, params) - response = PARSER.xml_in(http_get(request_url(method, params)), { 'keeproot' => false, + request_xml=http_get(request_url(method, params)) + response = PARSER.xml_in(request_xml, { 'keeproot' => false, 'forcearray' => %w[List Campaign Subscriber Client SubscriberOpen SubscriberUnsubscribe SubscriberClick SubscriberBounce], 'noattr' => true }) response.delete('d1p1:type') @@ -105,7 +107,8 @@ def request_url(method, params={}) # Does an HTTP GET on a given URL and returns the response body def http_get(url) - Net::HTTP.get_response(URI.parse(url)).body.to_s + response=Net::HTTP.get_response(URI.parse(url)) + response.body.to_s end # By overriding the method_missing method, it is possible to easily support all of the methods diff --git a/lib/campaign_monitor/base.rb b/lib/campaign_monitor/base.rb new file mode 100644 index 0000000..67e3278 --- /dev/null +++ b/lib/campaign_monitor/base.rb @@ -0,0 +1,21 @@ +class CampaignMonitor + # Provides access to the lists and campaigns associated with a client + class Base + + @@client=nil + + def self.client + @@client + end + + def self.client=(a) + @@client=a + end + + def initialize(*args) + @cm_client=@@client + end + end + +end + \ No newline at end of file diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index 35ed73d..aa80faf 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -1,6 +1,6 @@ class CampaignMonitor # Provides access to the lists and campaigns associated with a client - class Client + class Client < Base include CampaignMonitor::Helpers attr_reader :id, :name, :cm_client @@ -10,7 +10,7 @@ class Client def initialize(id, name=nil) @id = id @name = name - @cm_client = CampaignMonitor.new + super end # Example diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index 732435e..2c63032 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -3,7 +3,7 @@ class CampaignMonitor # Provides access to the subscribers and info about subscribers # associated with a Mailing List - class List + class List < Base include CampaignMonitor::Helpers attr_reader :id, :name, :cm_client @@ -13,7 +13,7 @@ class List def initialize(id=nil, name=nil) @id = id @name = name - @cm_client = CampaignMonitor.new + super end # Example diff --git a/lib/campaign_monitor/subscriber.rb b/lib/campaign_monitor/subscriber.rb index 15aa2a1..de4ca00 100644 --- a/lib/campaign_monitor/subscriber.rb +++ b/lib/campaign_monitor/subscriber.rb @@ -1,6 +1,6 @@ class CampaignMonitor # Provides the ability to add/remove subscribers from a list - class Subscriber + class Subscriber < Base include CampaignMonitor::Helpers attr_accessor :email_address, :name, :date_subscribed @@ -10,7 +10,7 @@ def initialize(email_address, name=nil, date=nil) @email_address = email_address @name = name @date_subscribed = date_subscribed - @cm_client = CampaignMonitor.new + super end # Example diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index c020bda..15c98a2 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -3,47 +3,83 @@ require 'test/unit' CAMPAIGN_MONITOR_API_KEY = 'Your API Key' -CLIENT_NAME = '_Test' -LIST_NAME = '_List1' +CLIENT_NAME = 'Spacely Space Sprockets' +LIST_NAME = 'List #1' class CampaignMonitorTest < Test::Unit::TestCase def setup - @cm = CampaignMonitor.new + @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) + # find an existing client + @client=find_test_client + assert_not_nil @client, "Please create a '#{CLIENT_NAME}' client so tests can run." + # create one list for that client + response = @cm.List_Create(build_new_list("ClientID" => @client.id)) + @list_id=response["__content__"] end - def test_clients + def teardown + response = @cm.List_Delete("ListID" => @list_id) + end + + # def test_create_and_delete_client + # before=@cm.clients.size + # response = @cm.Client_Create(build_new_client) + # puts response.inspect + # assert_equal before+1, @cm.clients.size + # @client_id=response["__content__"] + # reponse = @cm.Client_Delete("ClientID" => @client_id) + # assert_equal before, @cm.clients.size + # end + + def test_find_existing_client_by_name clients = @cm.clients assert clients.size > 0 - assert_equal CLIENT_NAME, find_test_client(clients).name + assert clients.map {|c| c.name}.include?(CLIENT_NAME), "could not find client named: #{CLIENT_NAME}" end + # def test_create_list + # list = @client.new_list + # list["Title"]="This a new list" + # assert_success list.create + # end + def test_lists - client = find_test_client - assert_not_nil client - - lists = client.lists + lists = @client.lists assert lists.size > 0 - list = find_test_list(lists) - assert_equal LIST_NAME, list.name + assert lists.map {|l| l.name}.include?(LIST_NAME), "could not find list named: #{LIST_NAME}" end def test_list_add_subscriber list = find_test_list - + assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size assert_success list.add_and_resubscribe('a@test.com', 'Test A') + assert_equal 1, list.active_subscribers(Date.new(2005,1,1)).size assert_success list.remove_subscriber('a@test.com') end def test_campaigns client = find_test_client - assert_not_nil client.campaigns + assert client.campaigns.size > 0, "should have one campaign" end protected + def build_new_client(options={}) + {"CompanyName" => "Spacely Space Sprockets", "ContactName" => "George Jetson", + "EmailAddress" => "george@sss.com", "Country" => "United States of America", + "TimeZone" => "(GMT-05:00) Indiana (East)" + }.merge(options) + end + + def build_new_list(options={}) + {"Title" => "List #1", "ConfirmOptIn" => "false", + "UnsubscribePage" => "", + "ConfirmationSuccessPage" => ""}.merge(options) + end + def assert_success(result) assert result.succeeded?, result.message end From 60765aa3fcfa5184727d7d8d7cae9b1da311858e Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 02:43:45 -0500 Subject: [PATCH 02/31] * move to more of an AR like attributes model where we can set and access attributes with [] * more the core API methods down into the list class (as a trial) so you can do things like: @client.new_list and then later @list.Create @list.Delete @list.Update @list.GetDetail --- lib/campaign_monitor/base.rb | 9 ++++++++ lib/campaign_monitor/client.rb | 4 ++++ lib/campaign_monitor/list.rb | 40 +++++++++++++++++++++++++++++++--- lib/campaign_monitor/result.rb | 4 +++- test/campaign_monitor_test.rb | 15 ++++++++----- 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/lib/campaign_monitor/base.rb b/lib/campaign_monitor/base.rb index 67e3278..a15a48f 100644 --- a/lib/campaign_monitor/base.rb +++ b/lib/campaign_monitor/base.rb @@ -12,7 +12,16 @@ def self.client=(a) @@client=a end + def [](k) + @attributes[k] + end + + def []=(k,v) + @attributes[k]=v + end + def initialize(*args) + @attributes={} @cm_client=@@client end end diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index aa80faf..aa5ae4c 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -13,6 +13,10 @@ def initialize(id, name=nil) super end + def new_list + List.new(nil,nil,:ClientID => id) + end + # Example # @client = new Client(12345) # @lists = @client.lists diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index 2c63032..bf59222 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -6,15 +6,49 @@ class CampaignMonitor class List < Base include CampaignMonitor::Helpers - attr_reader :id, :name, :cm_client + attr_reader :id, :cm_client, :result # Example # @list = new List(12345) - def initialize(id=nil, name=nil) + def initialize(id=nil, name=nil,attrs={}) + defaults={"ConfirmOptIn" => "false", + "UnsubscribePage" => "", + "ConfirmationSuccessPage" => ""} + super @id = id @name = name - super + @attributes=defaults.merge(attrs) + self["Title"]=name if name # override for now + end + + # compatible with previous API + def name + self["Title"] + end + + # AR like + def save + id ? Update : Create end + + def Update + # TODO: need to make sure we've loaded the full record with List_GetDetail + # so that we have the full component of attributes to write back to the API + @attributes["ListID"] ||= id + @result=Result.new(List_Update(@attributes)) + end + + def Delete + @result=Result.new(cm_client.List_Delete("ListID" => id)) + @result.success? + end + + def Create + raw=cm_client.List_Create(@attributes) + @result=Result.new(raw) + @id = raw["__content__"] if raw["__content__"] + @id ? true : false + end # Example # @list = new List(12345) diff --git a/lib/campaign_monitor/result.rb b/lib/campaign_monitor/result.rb index 7da9810..1226d41 100644 --- a/lib/campaign_monitor/result.rb +++ b/lib/campaign_monitor/result.rb @@ -8,10 +8,12 @@ def initialize(response) @code = response["Code"].to_i end - def succeeded? + def success? code == 0 end + alias :succeeded? :success? + def failed? !succeeded? end diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index 15c98a2..35013c5 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -39,11 +39,16 @@ def test_find_existing_client_by_name assert clients.map {|c| c.name}.include?(CLIENT_NAME), "could not find client named: #{CLIENT_NAME}" end - # def test_create_list - # list = @client.new_list - # list["Title"]="This a new list" - # assert_success list.create - # end + def test_create_list + list = @client.new_list + list["Title"]="This a new list" + assert list.Create + assert_success list.result + assert_not_nil list.id + assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size + assert list.Delete + assert_success list.result + end def test_lists lists = @client.lists From f93d54083b3a078152845f219d9a238dba85420b Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 03:32:06 -0500 Subject: [PATCH 03/31] new list api and tests --- lib/campaign_monitor.rb | 2 +- lib/campaign_monitor/client.rb | 2 +- lib/campaign_monitor/list.rb | 65 ++++++++++++++++++------- test/campaign_monitor_test.rb | 48 +++---------------- test/list_test.rb | 88 ++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 59 deletions(-) create mode 100644 test/list_test.rb diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 893eee3..901d504 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -166,7 +166,7 @@ def campaigns(client_id) # end def lists(client_id) handle_response(Client_GetLists("ClientID" => client_id)) do |response| - response["List"].collect{|l| List.new(l["ListID"], l["Name"])} + response["List"].collect{|l| List.new(l)} end end diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index aa5ae4c..118221f 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -14,7 +14,7 @@ def initialize(id, name=nil) end def new_list - List.new(nil,nil,:ClientID => id) + List.new(:ClientID => id) end # Example diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index bf59222..758ea6d 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -6,24 +6,47 @@ class CampaignMonitor class List < Base include CampaignMonitor::Helpers - attr_reader :id, :cm_client, :result + attr_reader :cm_client, :result # Example # @list = new List(12345) - def initialize(id=nil, name=nil,attrs={}) - defaults={"ConfirmOptIn" => "false", - "UnsubscribePage" => "", - "ConfirmationSuccessPage" => ""} + def initialize(attrs={}) super - @id = id - @name = name - @attributes=defaults.merge(attrs) - self["Title"]=name if name # override for now + @attributes=attrs end # compatible with previous API def name - self["Title"] + self["Title"] || self["Name"] + end + + def id + self["ListID"] + end + + # Example + # + # @list = @client.new_list.defaults + + def defaults + defaults={"ConfirmOptIn" => "false", + "UnsubscribePage" => "", + "ConfirmationSuccessPage" => ""} + @attributes=defaults.merge(@attributes) + self + end + + def []=(k,v) + if %w{Title Name}.include?(k) + super("Title", v) + super("Name", v) + else + super(k,v) + end + end + + def id=(v) + self["ListID"]=v end # AR like @@ -31,11 +54,21 @@ def save id ? Update : Create end + def GetDetail(overwrite=false) + raw=cm_client.List_GetDetail("ListID" => id) + @attributes=raw.merge(@attributes) + @attributes.merge!(raw) if overwrite + end + def Update - # TODO: need to make sure we've loaded the full record with List_GetDetail - # so that we have the full component of attributes to write back to the API - @attributes["ListID"] ||= id - @result=Result.new(List_Update(@attributes)) + # if we're dealing with a half baked object that Client#lists has given + # us then we need to popular all the fields before we can attempt an update + unless @fully_baked + self.GetDetail + @fully_baked=true + end + @result=Result.new(cm_client.List_Update(@attributes)) + @result.success? end def Delete @@ -46,8 +79,8 @@ def Delete def Create raw=cm_client.List_Create(@attributes) @result=Result.new(raw) - @id = raw["__content__"] if raw["__content__"] - @id ? true : false + self.id = raw["__content__"] if raw["__content__"] + id ? true : false end # Example diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index 35013c5..6586a6d 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -12,15 +12,9 @@ def setup @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) # find an existing client @client=find_test_client - assert_not_nil @client, "Please create a '#{CLIENT_NAME}' client so tests can run." - # create one list for that client - response = @cm.List_Create(build_new_list("ClientID" => @client.id)) - @list_id=response["__content__"] + assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." end - def teardown - response = @cm.List_Delete("ListID" => @list_id) - end # def test_create_and_delete_client # before=@cm.clients.size @@ -39,36 +33,13 @@ def test_find_existing_client_by_name assert clients.map {|c| c.name}.include?(CLIENT_NAME), "could not find client named: #{CLIENT_NAME}" end - def test_create_list - list = @client.new_list - list["Title"]="This a new list" - assert list.Create - assert_success list.result - assert_not_nil list.id - assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size - assert list.Delete - assert_success list.result - end - - def test_lists - lists = @client.lists - assert lists.size > 0 - - assert lists.map {|l| l.name}.include?(LIST_NAME), "could not find list named: #{LIST_NAME}" - end - def test_list_add_subscriber - list = find_test_list - assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size - assert_success list.add_and_resubscribe('a@test.com', 'Test A') - assert_equal 1, list.active_subscribers(Date.new(2005,1,1)).size - assert_success list.remove_subscriber('a@test.com') - end + # campaigns - def test_campaigns - client = find_test_client - assert client.campaigns.size > 0, "should have one campaign" - end + # def test_campaigns + # client = find_test_client + # assert client.campaigns.size > 0, "should have one campaign" + # end protected @@ -79,12 +50,7 @@ def build_new_client(options={}) }.merge(options) end - def build_new_list(options={}) - {"Title" => "List #1", "ConfirmOptIn" => "false", - "UnsubscribePage" => "", - "ConfirmationSuccessPage" => ""}.merge(options) - end - + def assert_success(result) assert result.succeeded?, result.message end diff --git a/test/list_test.rb b/test/list_test.rb new file mode 100644 index 0000000..fc858e4 --- /dev/null +++ b/test/list_test.rb @@ -0,0 +1,88 @@ +require 'rubygems' +require 'campaign_monitor' +require 'test/unit' + +CAMPAIGN_MONITOR_API_KEY = 'Your API Key' +CLIENT_NAME = 'Spacely Space Sprockets' +LIST_NAME = 'List #1' + +class CampaignMonitorTest < Test::Unit::TestCase + + def setup + @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) + # find an existing client + @client=find_test_client + assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." + + @list = @client.new_list.defaults + @list["Title"]="List #1" + assert @list.Create + end + + def teardown + @list.Delete + end + + def test_create_and_delete_list + list = @client.new_list.defaults + list["Title"]="This is a new list" + assert list.Create + assert_success list.result + assert_not_nil list.id + assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size + saved_id=list.id + # find it all over again + list=@client.lists.detect { |x| x.name == "This is a new list" } + assert_equal saved_id, list.id + assert list.Delete + assert_success list.result + # should be gone now + assert_nil @client.lists.detect { |x| x.name == "This is a new list" } + end + + def test_update_list + list=@client.lists.first + assert_equal "List #1", list.name + list["Name"]="Just another list" + list.Update + list=@client.lists.first + assert_equal "Just another list", list.name + end + + def test_getinfo_list + list=@client.lists.first + assert_equal "List #1", list.name + assert_nil list["ConfirmOptIn"] + list.GetDetail + assert_equal "false", list["ConfirmOptIn"] + end + + def test_list_add_subscriber + list=@client.lists.first + assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size + assert_success list.add_and_resubscribe('a@test.com', 'Test A') + assert_equal 1, list.active_subscribers(Date.new(2005,1,1)).size + assert_success list.remove_subscriber('a@test.com') + end + + + protected + + def build_new_list(options={}) + {"Title" => "List #1", "ConfirmOptIn" => "false", + "UnsubscribePage" => "", + "ConfirmationSuccessPage" => ""}.merge(options) + end + + def assert_success(result) + assert result.succeeded?, result.message + end + + def find_test_client(clients=@cm.clients) + clients.detect { |c| c.name == CLIENT_NAME } + end + + def find_test_list(lists=find_test_client.lists) + lists.detect { |l| l.name == LIST_NAME } + end +end \ No newline at end of file From c8af33ffec4239b121b50b4a992ceb802bddbd83 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 04:59:26 -0500 Subject: [PATCH 04/31] add List.GetDetail and List[] and catch XML parsing errors and masquerade them as API errors so they can be better dealt with --- lib/campaign_monitor.rb | 14 +++++++++----- lib/campaign_monitor/list.rb | 22 +++++++++++++++++++--- lib/campaign_monitor/result.rb | 11 +++++++++-- test/list_test.rb | 24 +++++++++++++++++++++++- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 901d504..0ae23de 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -87,11 +87,15 @@ def initialize(api_key=CAMPAIGN_MONITOR_API_KEY) # returns an XmlSimple object with the response def request(method, params) request_xml=http_get(request_url(method, params)) - response = PARSER.xml_in(request_xml, { 'keeproot' => false, - 'forcearray' => %w[List Campaign Subscriber Client SubscriberOpen SubscriberUnsubscribe SubscriberClick SubscriberBounce], - 'noattr' => true }) - response.delete('d1p1:type') - response + begin + response = PARSER.xml_in(request_xml, { 'keeproot' => false, + 'forcearray' => %w[List Campaign Subscriber Client SubscriberOpen SubscriberUnsubscribe SubscriberClick SubscriberBounce], + 'noattr' => true }) + response.delete('d1p1:type') + response + rescue XML::Parser::ParseError + { "Code" => 500, "Message" => request_xml.split(/\r?\n/).first, "FullError" => request_xml } + end end # Takes a CampaignMonitor API method name and set of parameters; returns the correct URL for the REST API. diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index 758ea6d..cdac284 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -53,11 +53,27 @@ def id=(v) def save id ? Update : Create end + + # Loads a list by it's ID + # + # @list = List.GetDetail(1234) + def self.GetDetail(id) + list=self.new("ListID" => id) + list.GetDetail(true) + list.result.code == 101 ? nil : list + end + + # loads a list by it's ID + # + # @list = List.GetDetail(1234) + def self.[](k) + GetDetail(k) + end def GetDetail(overwrite=false) - raw=cm_client.List_GetDetail("ListID" => id) - @attributes=raw.merge(@attributes) - @attributes.merge!(raw) if overwrite + @result=Result.new(cm_client.List_GetDetail("ListID" => id)) + @attributes=@result.raw.merge(@attributes) + @attributes.merge!(@result.raw) if overwrite end def Update diff --git a/lib/campaign_monitor/result.rb b/lib/campaign_monitor/result.rb index 1226d41..f0ba711 100644 --- a/lib/campaign_monitor/result.rb +++ b/lib/campaign_monitor/result.rb @@ -6,16 +6,23 @@ class Result def initialize(response) @message = response["Message"] @code = response["Code"].to_i + @raw=response end def success? code == 0 end - alias :succeeded? :success? - def failed? !succeeded? end + + alias :succeeded? :success? + alias :failure? :failed? + + def raw + @raw + end + end end \ No newline at end of file diff --git a/test/list_test.rb b/test/list_test.rb index fc858e4..b58611d 100644 --- a/test/list_test.rb +++ b/test/list_test.rb @@ -49,13 +49,35 @@ def test_update_list assert_equal "Just another list", list.name end - def test_getinfo_list + def test_getdetail_for_list_instance list=@client.lists.first assert_equal "List #1", list.name assert_nil list["ConfirmOptIn"] list.GetDetail assert_equal "false", list["ConfirmOptIn"] end + + def test_getdetail_to_load_list + list=CampaignMonitor::List.GetDetail(@list.id) + assert_equal "List #1", list.name + list=CampaignMonitor::List[@list.id] + assert_equal "List #1", list.name + end + + # test that our own creative mapping of errors actually works + def test_save_with_missing_attribute + list = @client.new_list + list["Title"]="This is a new list" + assert !list.Create + assert list.result.failure? + assert_equal 500, list.result.code + assert_equal "System.InvalidOperationException: Missing parameter: UnsubscribePage.", list.result.message + end + + def test_getting_a_dummy_list + list=CampaignMonitor::List["snickers"] + assert_equal nil, list + end def test_list_add_subscriber list=@client.lists.first From 8e09a22272ad3f74d027b57443fd0866c50da1f0 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 05:01:56 -0500 Subject: [PATCH 05/31] GetDetail should return success as well --- lib/campaign_monitor/list.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index cdac284..dddb39b 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -74,6 +74,7 @@ def GetDetail(overwrite=false) @result=Result.new(cm_client.List_GetDetail("ListID" => id)) @attributes=@result.raw.merge(@attributes) @attributes.merge!(@result.raw) if overwrite + @result.success? end def Update From 728e42088af789a582e8fc2ade064f7eb9b34681 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 05:05:13 -0500 Subject: [PATCH 06/31] clean up Create a bit --- lib/campaign_monitor/list.rb | 7 +++---- lib/campaign_monitor/result.rb | 4 ++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index dddb39b..7f728bb 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -94,10 +94,9 @@ def Delete end def Create - raw=cm_client.List_Create(@attributes) - @result=Result.new(raw) - self.id = raw["__content__"] if raw["__content__"] - id ? true : false + @result=Result.new(cm_client.List_Create(@attributes)) + self.id = @result.content if @result.success? + @result.success? end # Example diff --git a/lib/campaign_monitor/result.rb b/lib/campaign_monitor/result.rb index f0ba711..858afd9 100644 --- a/lib/campaign_monitor/result.rb +++ b/lib/campaign_monitor/result.rb @@ -17,6 +17,10 @@ def failed? !succeeded? end + def content + raw["__content__"] + end + alias :succeeded? :success? alias :failure? :failed? From e0a8bc1e716047e6bb50a54678e2a4a3629da899 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 05:16:26 -0500 Subject: [PATCH 07/31] add a test to deal with concurrent use issues --- lib/campaign_monitor.rb | 4 ++++ test/campaign_monitor_test.rb | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 0ae23de..b22e92d 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -136,6 +136,10 @@ def clients end end + def new_client + Client.new(nil) + end + def system_date User_GetSystemDate() end diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index 6586a6d..a9fc2d9 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -33,6 +33,16 @@ def test_find_existing_client_by_name assert clients.map {|c| c.name}.include?(CLIENT_NAME), "could not find client named: #{CLIENT_NAME}" end + # we should not get confused here + def test_can_access_two_accounts_at_once + @cm=CampaignMonitor.new("12345") + @cm2=CampaignMonitor.new("abcdef") + @client=@cm.new_client + @client2=@cm.new_client + assert_equal "12345", @client.cm_client.api_key + assert_equal "abcdef", @client2.cm_client.api_key + end + # campaigns From 8202e835543c26dec2274ea0693d4e5a941feead Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 16:54:27 -0500 Subject: [PATCH 08/31] add the client api --- lib/campaign_monitor.rb | 5 +- lib/campaign_monitor/base.rb | 29 +++++++- lib/campaign_monitor/client.rb | 76 ++++++++++++++++--- lib/campaign_monitor/list.rb | 29 ++------ support/class_enhancements.rb | 31 ++++++++ test/client_test.rb | 130 +++++++++++++++++++++++++++++++++ test/list_test.rb | 13 ++-- test/test_helper.rb | 7 ++ 8 files changed, 279 insertions(+), 41 deletions(-) create mode 100644 support/class_enhancements.rb create mode 100644 test/client_test.rb create mode 100644 test/test_helper.rb diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index b22e92d..dad997b 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -63,6 +63,7 @@ require 'xmlsimple' require 'date' +require File.join(File.dirname(__FILE__), '../support/class_enhancements.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/helpers.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/base.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/client.rb') @@ -132,7 +133,7 @@ def method_missing(method_id, params = {}) # end def clients handle_response(User_GetClients()) do |response| - response["Client"].collect{|c| Client.new(c["ClientID"], c["Name"])} + response["Client"].collect{|c| Client.new({"ClientID" => c["ClientID"], "CompanyName" => c["Name"]})} end end @@ -174,7 +175,7 @@ def campaigns(client_id) # end def lists(client_id) handle_response(Client_GetLists("ClientID" => client_id)) do |response| - response["List"].collect{|l| List.new(l)} + response["List"].collect{|l| List.new({"ListID" => l["ListID"], "Title" => l["Name"]})} end end diff --git a/lib/campaign_monitor/base.rb b/lib/campaign_monitor/base.rb index a15a48f..8f487aa 100644 --- a/lib/campaign_monitor/base.rb +++ b/lib/campaign_monitor/base.rb @@ -2,18 +2,24 @@ class CampaignMonitor # Provides access to the lists and campaigns associated with a client class Base + attr_reader :result + @@client=nil def self.client @@client end - + def self.client=(a) @@client=a end def [](k) - @attributes[k] + if m=self.class.get_data_types[k] + @attributes[k].send(m) + else + @attributes[k] + end end def []=(k,v) @@ -24,6 +30,25 @@ def initialize(*args) @attributes={} @cm_client=@@client end + + # id and name field stuff + + inherited_property "id_field", "id" + inherited_property "name_field", "name" + inherited_property "data_types", {} + + def id + @attributes[self.class.get_id_field] + end + + def id=(v) + @attributes[self.class.get_id_field]=v + end + + def name + @attributes[self.class.get_name_field] + end + end end diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index 118221f..a1971cb 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -1,20 +1,32 @@ class CampaignMonitor # Provides access to the lists and campaigns associated with a client + class ClientLists < Array + def initialize(v,parent) + @parent=parent + super(v) + end + def build(attrs={}) + List.new(attrs.merge(:ClientID => @parent.id)) + end + end + class Client < Base include CampaignMonitor::Helpers + id_field "ClientID" + name_field "CompanyName" - attr_reader :id, :name, :cm_client + data_types "AccessLevel" => "to_i" + + # we will assume if something isn't a basic attribute that it's a AccessAndBilling attribute + BASIC_ATTRIBUTES=%w{CompanyName ContactName EmailAddress Country Timezone} + + attr_reader :cm_client # Example # @client = new Client(12345) - def initialize(id, name=nil) - @id = id - @name = name + def initialize(attrs={}) super - end - - def new_list - List.new(:ClientID => id) + @attributes=attrs end # Example @@ -25,7 +37,7 @@ def new_list # puts list.name # end def lists - cm_client.lists(self.id) + ClientLists.new(cm_client.lists(self.id), self) end # Example @@ -38,5 +50,51 @@ def lists def campaigns cm_client.campaigns(self.id) end + + ####### + # API + ####### + + def GetDetail(overwrite=false) + @result=Result.new(cm_client.Client_GetDetail("ClientID" => id)) + @flatten={} + @flatten.merge!(@result.raw["BasicDetails"]) + @flatten.merge!(@result.raw["AccessAndBilling"]) + # TODO - look into + # map {} to nil - some weird XML converstion issue? + @flatten=@flatten.inject({}) { |sum,a| sum[a[0]]=a[1]=={} ? nil : a[1]; sum } + @attributes=@flatten.merge(@attributes) + @attributes.merge!(@flatten.raw) if overwrite + @result.success? + end + + # do a full update + def update + self.UpdateBasics + self.UpdateAccessAndBilling if result.success? + @result.success? + end + + def UpdateAccessAndBilling + fully_bake + @result=Result.new(cm_client.Client_UpdateAccessAndBilling(@attributes)) + @result.success? + end + + def UpdateBasics + fully_bake + @result=Result.new(cm_client.Client_UpdateBasics(@attributes)) + @result.success? + end + + private + + def fully_bake + unless @fully_baked + self.GetDetail + @fully_baked=true + end + end + end end \ No newline at end of file diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index 7f728bb..0267cba 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -6,7 +6,12 @@ class CampaignMonitor class List < Base include CampaignMonitor::Helpers - attr_reader :cm_client, :result + attr_reader :cm_client + + id_field "ListID" + name_field "Title" + + VALID_ATTRIBUTES=%w{ConfirmOptIn UnsubscribePage ConfirmationSuccessPage ListID Title} # Example # @list = new List(12345) @@ -15,15 +20,6 @@ def initialize(attrs={}) @attributes=attrs end - # compatible with previous API - def name - self["Title"] || self["Name"] - end - - def id - self["ListID"] - end - # Example # # @list = @client.new_list.defaults @@ -36,19 +32,6 @@ def defaults self end - def []=(k,v) - if %w{Title Name}.include?(k) - super("Title", v) - super("Name", v) - else - super(k,v) - end - end - - def id=(v) - self["ListID"]=v - end - # AR like def save id ? Update : Create diff --git a/support/class_enhancements.rb b/support/class_enhancements.rb new file mode 100644 index 0000000..5b83333 --- /dev/null +++ b/support/class_enhancements.rb @@ -0,0 +1,31 @@ +class Class + +def inherited_property(accessor, default = nil) + instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + @#{accessor} = default + + def set_#{accessor}(value) + @#{accessor} = value + end + alias #{accessor} set_#{accessor} + + def get_#{accessor} + return @#{accessor} if instance_variable_defined?(:@#{accessor}) + superclass.send(:get_#{accessor}) + end + RUBY + + # @path = default + # + # def set_path(value) + # @path = value + # end + # alias_method path, set_path + + # def get_path + # return @path if instance_variable_defined?(:path) + # superclass.send(:path) + # end + end + +end \ No newline at end of file diff --git a/test/client_test.rb b/test/client_test.rb new file mode 100644 index 0000000..1a1b2ea --- /dev/null +++ b/test/client_test.rb @@ -0,0 +1,130 @@ +require 'rubygems' +require 'campaign_monitor' +require 'test/unit' +require 'test/test_helper' + +CAMPAIGN_MONITOR_API_KEY = 'Your API Key' +CLIENT_NAME = 'Spacely Space Sprockets' +CLIENT_CONTACT_NAME = 'George Jetson' +LIST_NAME = 'List #1' + +class CampaignMonitorTest < Test::Unit::TestCase + + def setup + @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) + # find an existing client and make sure we know it's values + @client=find_test_client + assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." + end + + + def teardown + # revert it back + @client["ContactName"]="George Jetson" + @client["EmailAddress"]="george@sss.com" + + @client["Username"]="" + @client["Password"]="" + @client["AccessLevel"]=0 + + + assert @client.update + assert_success @client.result + end + + # def test_create_and_delete_client + # before=@cm.clients.size + # response = @cm.Client_Create(build_new_client) + # puts response.inspect + # assert_equal before+1, @cm.clients.size + # @client_id=response["__content__"] + # reponse = @cm.Client_Delete("ClientID" => @client_id) + # assert_equal before, @cm.clients.size + # end + + def test_find_existing_client_by_name + clients = @cm.clients + assert clients.size > 0 + + assert clients.map {|c| c.name}.include?(CLIENT_NAME), "could not find client named: #{CLIENT_NAME}" + end + + def test_client_attributes + assert_equal CLIENT_NAME, @client["CompanyName"] + assert_nil @client["ContactName"] + assert @client.GetDetail + assert_not_empty @client["ContactName"] + assert_not_empty @client["EmailAddress"] + assert_not_empty @client["Country"] + assert_not_empty @client["Timezone"] + + assert_nil @client["Username"] + assert_nil @client["Password"] + assert_equal 0, @client["AccessLevel"] + end + + def test_update_client_basics + # only update the name + @client["ContactName"]="Bob Watson" + assert @client.UpdateBasics + assert_success @client.result + client=@cm.clients.detect {|x| x.name==CLIENT_NAME} + client.GetDetail + assert_equal "Bob Watson", client["ContactName"] + # make sure e-mail has remained unchanged + assert_equal "george@sss.com", client["EmailAddress"] + end + + def test_update_access_and_billing + @client["Username"]="login" + @client["Password"]="secret" + @client["AccessLevel"]=63 + @client["BillingType"]="ClientPaysAtStandardRate" + @client["Currency"]="USD" + @client.UpdateAccessAndBilling + assert_success @client.result + # load it up again + client=@cm.clients.detect {|x| x.name==CLIENT_NAME} + client.GetDetail + assert_equal "login", client["Username"] + assert_equal "secret", client["Password"] + assert_equal "ClientPaysAtStandardRate", client["BillingType"] + assert_equal 63, client["AccessLevel"] + assert_equal "USD", client["Currency"] + end + + def test_update_both + @client["ContactName"]="Bob Watson" + @client["Username"]="login" + @client["Password"]="secret" + @client["AccessLevel"]=63 + @client["BillingType"]="ClientPaysAtStandardRate" + @client["Currency"]="USD" + @client.update + assert_success @client.result + assert_equal "login", @client["Username"] + assert_equal "Bob Watson", @client["ContactName"] + end + + + protected + def build_new_client(options={}) + {"CompanyName" => "Spacely Space Sprockets", "ContactName" => "George Jetson", + "EmailAddress" => "george@sss.com", "Country" => "United States of America", + "TimeZone" => "(GMT-05:00) Indiana (East)" + }.merge(options) + end + + + def assert_success(result) + assert result.succeeded?, result.message + end + + def find_test_client(clients=@cm.clients) + clients.detect { |c| c.name == CLIENT_NAME } + end + + def find_test_list(lists=find_test_client.lists) + lists.detect { |l| l.name == LIST_NAME } + end +end \ No newline at end of file diff --git a/test/list_test.rb b/test/list_test.rb index b58611d..5d436a6 100644 --- a/test/list_test.rb +++ b/test/list_test.rb @@ -11,10 +11,13 @@ class CampaignMonitorTest < Test::Unit::TestCase def setup @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) # find an existing client + @client=find_test_client assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." - @list = @client.new_list.defaults + # delete all existing lists + @client.lists.each { |l| l.Delete } + @list = @client.lists.build.defaults @list["Title"]="List #1" assert @list.Create end @@ -24,9 +27,9 @@ def teardown end def test_create_and_delete_list - list = @client.new_list.defaults + list = @client.lists.build.defaults list["Title"]="This is a new list" - assert list.Create + list.Create assert_success list.result assert_not_nil list.id assert_equal 0, list.active_subscribers(Date.new(2005,1,1)).size @@ -43,7 +46,7 @@ def test_create_and_delete_list def test_update_list list=@client.lists.first assert_equal "List #1", list.name - list["Name"]="Just another list" + list["Title"]="Just another list" list.Update list=@client.lists.first assert_equal "Just another list", list.name @@ -66,7 +69,7 @@ def test_getdetail_to_load_list # test that our own creative mapping of errors actually works def test_save_with_missing_attribute - list = @client.new_list + list = @client.lists.build list["Title"]="This is a new list" assert !list.Create assert list.result.failure? diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..a76e84d --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,7 @@ +class Test::Unit::TestCase + + def assert_not_empty(v) + assert !v.nil? and !v=="" + end + +end \ No newline at end of file From 7c95fb560fdbbf58de279e8fb54558d481f5a653 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 16:58:35 -0500 Subject: [PATCH 09/31] update gemspec --- campaign_monitor.gemspec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index c6fa483..fe489fb 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |s| 'README.rdoc', 'lib/campaign_monitor.rb', + 'lib/campaign_monitor/base.rb', 'lib/campaign_monitor/campaign.rb', 'lib/campaign_monitor/client.rb', 'lib/campaign_monitor/helpers.rb', @@ -32,12 +33,16 @@ Gem::Specification.new do |s| 'lib/campaign_monitor/result.rb', 'lib/campaign_monitor/subscriber.rb', + 'support/class_enhancements.rb', 'support/faster-xml-simple/lib/faster_xml_simple.rb', 'support/faster-xml-simple/test/regression_test.rb', 'support/faster-xml-simple/test/test_helper.rb', 'support/faster-xml-simple/test/xml_simple_comparison_test.rb', 'test/campaign_monitor_test.rb', + 'test/client_test.rb', + 'test/list_test.rb', + 'test/test_helper.rb' ] s.test_file = 'test/campaign_monitor_test.rb' From 0a36f70ee80ab433a5bffd497944dcc7a4f930fb Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 17:10:41 -0500 Subject: [PATCH 10/31] tests for adding and deleting clients with the ruby api --- lib/campaign_monitor/client.rb | 11 +++++++++++ test/client_test.rb | 13 +++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index a1971cb..8b55d09 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -86,6 +86,17 @@ def UpdateBasics @result=Result.new(cm_client.Client_UpdateBasics(@attributes)) @result.success? end + + def Create + @result=Result.new(cm_client.Client_Create(@attributes)) + self.id = @result.content + @result.success? + end + + def Delete + @result=Result.new(cm_client.Client_Delete("ClientID" => id)) + @result.success? + end private diff --git a/test/client_test.rb b/test/client_test.rb index 1a1b2ea..a48b252 100644 --- a/test/client_test.rb +++ b/test/client_test.rb @@ -34,11 +34,12 @@ def teardown # def test_create_and_delete_client # before=@cm.clients.size - # response = @cm.Client_Create(build_new_client) - # puts response.inspect + # client=CampaignMonitor::Client.new(build_new_client) + # client.Create + # assert_success client.result # assert_equal before+1, @cm.clients.size - # @client_id=response["__content__"] - # reponse = @cm.Client_Delete("ClientID" => @client_id) + # client.Delete + # assert_success client.result # assert_equal before, @cm.clients.size # end @@ -109,8 +110,8 @@ def test_update_both protected def build_new_client(options={}) - {"CompanyName" => "Spacely Space Sprockets", "ContactName" => "George Jetson", - "EmailAddress" => "george@sss.com", "Country" => "United States of America", + {"CompanyName" => "Lick More Enterprises", "ContactName" => "George Jetson", + "EmailAddress" => "george@jetson.com", "Country" => "United States of America", "TimeZone" => "(GMT-05:00) Indiana (East)" }.merge(options) end From c84d0125526fc1ddec9d686f0e993cae3169ee5e Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 17:22:29 -0500 Subject: [PATCH 11/31] remove a line --- test/client_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/client_test.rb b/test/client_test.rb index a48b252..ee9b958 100644 --- a/test/client_test.rb +++ b/test/client_test.rb @@ -27,7 +27,6 @@ def teardown @client["Password"]="" @client["AccessLevel"]=0 - assert @client.update assert_success @client.result end From d8524dc4787fb311c2a32f7966b3ab7b73fa5aed Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 17:23:25 -0500 Subject: [PATCH 12/31] try to change it just to see --- campaign_monitor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index fe489fb..03b845c 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -46,4 +46,4 @@ Gem::Specification.new do |s| ] s.test_file = 'test/campaign_monitor_test.rb' -end \ No newline at end of file +end From 61a9e18038a45c3ce59bd78bd03f0dc73ecbdf38 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 19:16:51 -0500 Subject: [PATCH 13/31] update gemspec --- campaign_monitor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index 03b845c..fe489fb 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -46,4 +46,4 @@ Gem::Specification.new do |s| ] s.test_file = 'test/campaign_monitor_test.rb' -end +end \ No newline at end of file From 07581bf2651b9f8fbda4ad04cedc26b02ee5bce1 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 20:29:44 -0500 Subject: [PATCH 14/31] better docs for Client --- lib/campaign_monitor/client.rb | 141 +++++++++++++++++++++++++++++---- test/test_helper.rb | 4 +- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index 8b55d09..1bbdc70 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -10,6 +10,34 @@ def build(attrs={}) end end + # The Client class aims to impliment the full functionality of the CampaignMonitor + # Clients API as detailed at: http://www.campaignmonitor.com/api/ + # === Attributes + # + # Attriutes can be read and set as if Client were a Hash + # + # @client["CompanyName"]="Road Running, Inc." + # @client["ContactName"] => "Wiley Coyote" + # + # Convenience attribute readers are provided for name and id + # + # @client.id == @client["ClientID"] + # @client.name == @client["CompanyName"] + # + # === API calls supported + # + # * Client.Create + # * Client.Delete + # * Client.GetCampaigns + # * Client.GetDetail + # * Client.GetLists + # * Client.UpdateAccessAndBilling + # * Client.UpdateBasics + # + # === Not yet supported + # + # * Client.GetSegments - TODO + # * Client.GetSuppressionList - TODO class Client < Base include CampaignMonitor::Helpers id_field "ClientID" @@ -22,39 +50,79 @@ class Client < Base attr_reader :cm_client + # Creates a new client that you can later create (or load) + # The prefered way to load a client is using Client#[] however + # # Example - # @client = new Client(12345) + # + # @client = Client.new(attributes) + # @client.Create + # + # @client = Client.new("ClientID" => 12345) + # @client.GetDetails def initialize(attrs={}) super @attributes=attrs end + # Calls Client.GetLists and returns a collection of CM::Campaign objects + # # Example - # @client = new Client(12345) + # @client = @cm.clients.first + # @new_list = @client.lists.build # @lists = @client.lists # # for list in @lists - # puts list.name + # puts list.name # a shortcut for list["Title"] # end - def lists + def GetLists ClientLists.new(cm_client.lists(self.id), self) end + alias lists GetLists + + # Calls Client.GetCampaigns and returns a collection of CM::List objects + # # Example - # @client = new Client(12345) + # @client = @cm.clients.first # @campaigns = @client.campaigns # # for campaign in @campaigns # puts campaign.subject # end - def campaigns + def GetCampaigns cm_client.campaigns(self.id) end + + alias campaigns GetCampaigns - ####### - # API - ####### + # Calls Client.GetDetails to load a specific client + # Client#result will have the result of the API call + # + # Example + # + # @client=Client[12345] + # puts @client.name if @client.result.success? + def self.[] + client=self.new("ClientID" => id) + client.GetDetail(true) + client.result.code == 101 ? nil : client + end + + # Calls Client.GetDetails + # This is needed because often if you're working with a list of clients you really only + # have their company name when what you want is the full record. + # It will return true if successful and false if not. + # Client#result will have the result of the API call + # + # Example + # + # @client=@cm.clients.first + # @client["CompanyName"]="Ben's Widgets" + # @client["ContactName"] => nil + # @client.GetDetail + # @client["ContactName"] => "Ben Wilder" def GetDetail(overwrite=false) @result=Result.new(cm_client.Client_GetDetail("ClientID" => id)) @flatten={} @@ -68,31 +136,75 @@ def GetDetail(overwrite=false) @result.success? end - # do a full update + # This is just a convenience method that calls both Client.UpdateBasics and Client.UpdateAccessAndBilling. + # It will return true if successful and false if not. + # Client#result will have the result of the API call + # + # Example + # @client=@cm.clients.first + # @client["CompanyName"]="Ben's Widgets" + # @client.update def update self.UpdateBasics self.UpdateAccessAndBilling if result.success? @result.success? end - + + # Calls Client.UpdateAccessAndBilling + # This will also call GetDetails first to prepoluate any empty fields the API call needs + # It will return true if successful and false if not. + # Client#result will have the result of the API call + # + # Example + # @client=@cm.clients.first + # @client["Currency"]="USD" + # @client.UpdateAccessAndBilling def UpdateAccessAndBilling fully_bake @result=Result.new(cm_client.Client_UpdateAccessAndBilling(@attributes)) @result.success? end - + + # Calls Client.UpdateBasics + # This will also call GetDetails first to prepoluate any empty fields the API call needs + # It will return true if successful and false if not. + # Client#result will have the result of the API call + # + # Example + # @client=@cm.clients.first + # @client["CompanyName"]="Ben's Widgets" + # @client.UpdateBasics def UpdateBasics fully_bake @result=Result.new(cm_client.Client_UpdateBasics(@attributes)) @result.success? end + # Calls Client.Create + # It will return true if successful and false if not. + # Client#result will have the result of the API call + # + # Example + # @client=CampaignMonitor::Client.new + # @client["CompanyName"]="Ben's Widgets" + # @client["ContactName"]="Ben Winters" + # @client["Country"]=@cm.countries.first + # @client["Timezone"]=@cm.timezones.first + # ... + # @client.Create def Create @result=Result.new(cm_client.Client_Create(@attributes)) self.id = @result.content @result.success? end - + + # Calls Client.Delete. + # It will return true if successful and false if not. + # Client#result will have the result of the API call + # + # Example + # @client=@cm.clients.first + # @client.Delete def Delete @result=Result.new(cm_client.Client_Delete("ClientID" => id)) @result.success? @@ -100,7 +212,8 @@ def Delete private - def fully_bake + #:nodoc: + def fully_bake unless @fully_baked self.GetDetail @fully_baked=true diff --git a/test/test_helper.rb b/test/test_helper.rb index a76e84d..05211dc 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,7 +1,9 @@ class Test::Unit::TestCase def assert_not_empty(v) - assert !v.nil? and !v=="" + assert !v.nil?, "expected to not be empty, but was nil" + assert !v.empty?, "expected to not be empty" if v.respond_to?(:empty?) + assert !v.strip.empty?, "expected to not be empty" if v.is_a?(String) end end \ No newline at end of file From 57031136573e0fd75de5596c7cc4e5ad5a49de57 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 20:39:35 -0500 Subject: [PATCH 15/31] clean up rdoc --- lib/campaign_monitor.rb | 8 ++++---- support/class_enhancements.rb | 6 +++++- support/faster-xml-simple/test/regression_test.rb | 2 +- test/test_helper.rb | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index dad997b..d56a021 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -201,7 +201,7 @@ def using_soap end # Encapsulates - class SubscriberBounce + class SubscriberBounce #:nodoc: attr_reader :email_address, :bounce_type, :list_id def initialize(email_address, list_id, bounce_type) @@ -212,7 +212,7 @@ def initialize(email_address, list_id, bounce_type) end # Encapsulates - class SubscriberOpen + class SubscriberOpen #:nodoc: attr_reader :email_address, :list_id, :opens def initialize(email_address, list_id, opens) @@ -223,7 +223,7 @@ def initialize(email_address, list_id, opens) end # Encapsulates - class SubscriberClick + class SubscriberClick #:nodoc: attr_reader :email_address, :list_id, :clicked_links def initialize(email_address, list_id, clicked_links) @@ -234,7 +234,7 @@ def initialize(email_address, list_id, clicked_links) end # Encapsulates - class SubscriberUnsubscribe + class SubscriberUnsubscribe #:nodoc: attr_reader :email_address, :list_id def initialize(email_address, list_id) diff --git a/support/class_enhancements.rb b/support/class_enhancements.rb index 5b83333..61805e9 100644 --- a/support/class_enhancements.rb +++ b/support/class_enhancements.rb @@ -1,4 +1,4 @@ -class Class +module ClassEnhancements def inherited_property(accessor, default = nil) instance_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -28,4 +28,8 @@ def get_#{accessor} # end end +end + +class Class #:nodoc: + include ClassEnhancements end \ No newline at end of file diff --git a/support/faster-xml-simple/test/regression_test.rb b/support/faster-xml-simple/test/regression_test.rb index 5fde070..1172763 100644 --- a/support/faster-xml-simple/test/regression_test.rb +++ b/support/faster-xml-simple/test/regression_test.rb @@ -1,6 +1,6 @@ require File.dirname(__FILE__) + '/test_helper' -class RegressionTest < FasterXSTest +class RegressionTest < FasterXSTest #:nodoc: all def test_content_nil_regressions expected = {"asdf"=>{"jklsemicolon"=>{}}} assert_equal expected, FasterXmlSimple.xml_in("") diff --git a/test/test_helper.rb b/test/test_helper.rb index 05211dc..a1ac2e6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,4 @@ -class Test::Unit::TestCase +class Test::Unit::TestCase #:nodoc: all def assert_not_empty(v) assert !v.nil?, "expected to not be empty, but was nil" From 2b50d47020cf7fbef6ebce4fb64b8cd98e81def0 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 20:53:31 -0500 Subject: [PATCH 16/31] add countries and timezone calls --- lib/campaign_monitor.rb | 12 ++++++++++++ test/campaign_monitor_test.rb | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index d56a021..e6758ab 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -149,6 +149,18 @@ def parsed_system_date DateTime.strptime(system_date, timestamp_format) end + def countries + handle_response(User_GetCountries()) do | response | + response["string"] + end + end + + def timezones + handle_response(User_GetTimezones()) do | response | + response["string"] + end + end + # Returns an array of Campaign objects associated with the specified Client ID # # Example diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index a9fc2d9..04c5df4 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -43,6 +43,16 @@ def test_can_access_two_accounts_at_once assert_equal "abcdef", @client2.cm_client.api_key end + def test_timezones + assert_equal 90, @cm.timezones.length + end + + def test_countries + countries=@cm.countries + assert_equal 246, countries.length + assert countries.include?("United States of America") + end + # campaigns From 9a3700a5b994fd335dd8a591afc641896f270d41 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 21:00:10 -0500 Subject: [PATCH 17/31] rake test task actually runs tests now --- Rakefile | 9 ++++++++- test/campaign_monitor_test.rb | 4 ++-- test/client_test.rb | 3 +-- test/list_test.rb | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Rakefile b/Rakefile index 709a1d6..c6c6c23 100644 --- a/Rakefile +++ b/Rakefile @@ -15,9 +15,16 @@ task :install => [:package] do sh %{sudo gem install pkg/#{spec.name}-#{spec.version}} end +task :default => [:test] + Rake::TestTask.new do |t| + if ENV["API_KEY"].nil? + puts "Please specify the API_KEY on the command line for testing." + exit + end + t.libs << "test" - t.test_files = FileList['test/test*.rb'] + t.test_files = FileList['test/*_test.rb'] t.verbose = true end diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index 04c5df4..d7acfa5 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -1,15 +1,15 @@ require 'rubygems' require 'campaign_monitor' require 'test/unit' +require 'test/test_helper' -CAMPAIGN_MONITOR_API_KEY = 'Your API Key' CLIENT_NAME = 'Spacely Space Sprockets' LIST_NAME = 'List #1' class CampaignMonitorTest < Test::Unit::TestCase def setup - @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) + @cm = CampaignMonitor.new(ENV["API_KEY"]) # find an existing client @client=find_test_client assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." diff --git a/test/client_test.rb b/test/client_test.rb index ee9b958..d2cc241 100644 --- a/test/client_test.rb +++ b/test/client_test.rb @@ -3,7 +3,6 @@ require 'test/unit' require 'test/test_helper' -CAMPAIGN_MONITOR_API_KEY = 'Your API Key' CLIENT_NAME = 'Spacely Space Sprockets' CLIENT_CONTACT_NAME = 'George Jetson' LIST_NAME = 'List #1' @@ -11,7 +10,7 @@ class CampaignMonitorTest < Test::Unit::TestCase def setup - @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) + @cm = CampaignMonitor.new(ENV["API_KEY"]) # find an existing client and make sure we know it's values @client=find_test_client assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." diff --git a/test/list_test.rb b/test/list_test.rb index 5d436a6..aff5ad7 100644 --- a/test/list_test.rb +++ b/test/list_test.rb @@ -1,15 +1,15 @@ require 'rubygems' require 'campaign_monitor' require 'test/unit' +require 'test/test_helper' -CAMPAIGN_MONITOR_API_KEY = 'Your API Key' CLIENT_NAME = 'Spacely Space Sprockets' LIST_NAME = 'List #1' class CampaignMonitorTest < Test::Unit::TestCase def setup - @cm = CampaignMonitor.new(ENV["API_KEY"] || CAMPAIGN_MONITOR_API_KEY) + @cm = CampaignMonitor.new(ENV["API_KEY"]) # find an existing client @client=find_test_client From 243334c85ae06935ea13a4ccc5bdc241eacf1421 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 20 Feb 2009 21:21:35 -0500 Subject: [PATCH 18/31] clean up docs and add invalidkey error that's raised --- Rakefile | 5 --- lib/campaign_monitor.rb | 67 ++++++++++++++++++++------------- lib/campaign_monitor/helpers.rb | 2 + test/campaign_monitor_test.rb | 5 +++ test/test_helper.rb | 5 +++ 5 files changed, 53 insertions(+), 31 deletions(-) diff --git a/Rakefile b/Rakefile index c6c6c23..4d1e880 100644 --- a/Rakefile +++ b/Rakefile @@ -18,11 +18,6 @@ end task :default => [:test] Rake::TestTask.new do |t| - if ENV["API_KEY"].nil? - puts "Please specify the API_KEY on the command line for testing." - exit - end - t.libs << "test" t.test_files = FileList['test/*_test.rb'] t.verbose = true diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index e6758ab..6037afa 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -1,4 +1,18 @@ -# CampaignMonitor +require 'rubygems' +require 'cgi' +require 'net/http' +require 'xmlsimple' +require 'date' + +require File.join(File.dirname(__FILE__), '../support/class_enhancements.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/helpers.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/base.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/client.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/list.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/subscriber.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/result.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/campaign.rb') + # A wrapper class to access the Campaign Monitor API. Written using the wonderful # Flickr interface by Scott Raymond as a guide on how to access remote web services # @@ -18,20 +32,34 @@ # cm.lists(client_id) # cm.add_subscriber(list_id, email, name) # -# CLIENT -# client = Client.new(client_id) -# client.lists -# client.campaigns +# == CLIENT +# client = Client[client_id] # find an existing client +# client = Client.new(attributes) +# client.Create +# client.Delete +# client.GetDetail +# client.UpdateAccessAndBilling +# client.UpdateBasics +# client.update # update basics, access, and billing +# client.lists # OR +# client.GetLists +# client.lists.build # to create a new unsaved list for a client +# client.campaigns # OR +# client.GetCampaigns # -# LIST -# list = List.new(list_id) +# == LIST +# list = List[list_id] # find an existing list +# list = List.new(attributes) +# list.Create +# list.Delete +# list.Update # list.add_subscriber(email, name) # list.remove_subscriber(email) # list.active_subscribers(date) # list.unsubscribed(date) # list.bounced(date) # -# CAMPAIGN +# == CAMPAIGN # campaign = Campaign.new(campaign_id) # campaign.clicks # campaign.opens @@ -44,37 +72,24 @@ # campaign.number_unsubscribes # # -# SUBSCRIBER +# == SUBSCRIBER # subscriber = Subscriber.new(email) # subscriber.add(list_id) # subscriber.unsubscribe(list_id) # -# Data Types +# == Data Types # SubscriberBounce # SubscriberClick # SubscriberOpen # SubscriberUnsubscribe # Result # - -require 'rubygems' -require 'cgi' -require 'net/http' -require 'xmlsimple' -require 'date' - -require File.join(File.dirname(__FILE__), '../support/class_enhancements.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/helpers.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/base.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/client.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/list.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/subscriber.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/result.rb') -require File.join(File.dirname(__FILE__), 'campaign_monitor/campaign.rb') - class CampaignMonitor include CampaignMonitor::Helpers + class InvalidAPIKey < StandardError + end + attr_reader :api_key, :api_url # Replace this API key with your own (http://www.campaignmonitor.com/api/) diff --git a/lib/campaign_monitor/helpers.rb b/lib/campaign_monitor/helpers.rb index b766896..ccdf4b3 100644 --- a/lib/campaign_monitor/helpers.rb +++ b/lib/campaign_monitor/helpers.rb @@ -7,6 +7,8 @@ def handle_response(response) if response["Code"].to_i == 0 # success! yield(response) + elsif response["Code"].to_i == 100 + raise InvalidAPIKey else # error! raise response["Code"] + " - " + response["Message"] diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index d7acfa5..8b3c276 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -33,6 +33,11 @@ def test_find_existing_client_by_name assert clients.map {|c| c.name}.include?(CLIENT_NAME), "could not find client named: #{CLIENT_NAME}" end + def test_invalid_key + @cm=CampaignMonitor.new("12345") + assert_raises (CampaignMonitor::InvalidAPIKey) { @cm.clients } + end + # we should not get confused here def test_can_access_two_accounts_at_once @cm=CampaignMonitor.new("12345") diff --git a/test/test_helper.rb b/test/test_helper.rb index a1ac2e6..6d74735 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,8 @@ +if ENV["API_KEY"].nil? + puts "Please specify the API_KEY on the command line for testing." + exit +end + class Test::Unit::TestCase #:nodoc: all def assert_not_empty(v) From 0894b3a3d00ab1085e61ccab54e22a5862a61337 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Tue, 24 Feb 2009 21:22:37 -0500 Subject: [PATCH 19/31] need to accept id as a parameter --- lib/campaign_monitor/client.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index 1bbdc70..a6b9199 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -104,7 +104,7 @@ def GetCampaigns # # @client=Client[12345] # puts @client.name if @client.result.success? - def self.[] + def self.[](id) client=self.new("ClientID" => id) client.GetDetail(true) client.result.code == 101 ? nil : client From 86bec6c8acacd44b5e3b39c47148e01123284e95 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Wed, 25 Feb 2009 02:26:44 -0500 Subject: [PATCH 20/31] add some request debugging as well as fix some errors --- lib/campaign_monitor.rb | 5 ++++- lib/campaign_monitor/client.rb | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 6037afa..8c0901c 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -134,7 +134,10 @@ def http_get(url) # By overriding the method_missing method, it is possible to easily support all of the methods # available in the API def method_missing(method_id, params = {}) - request(method_id.id2name.gsub(/_/, '.'), params) + puts " CM: #{method_id} (#{params.inspect})" if $debug + res=request(method_id.id2name.gsub(/_/, '.'), params) + puts " returning: #{res.inspect}" if $debug + res end # Returns an array of Client objects associated with the API Key diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index a6b9199..2ddf261 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -107,7 +107,7 @@ def GetCampaigns def self.[](id) client=self.new("ClientID" => id) client.GetDetail(true) - client.result.code == 101 ? nil : client + client.result.code == 102 ? nil : client end # Calls Client.GetDetails @@ -125,6 +125,7 @@ def self.[](id) # @client["ContactName"] => "Ben Wilder" def GetDetail(overwrite=false) @result=Result.new(cm_client.Client_GetDetail("ClientID" => id)) + return false if @result.failed? @flatten={} @flatten.merge!(@result.raw["BasicDetails"]) @flatten.merge!(@result.raw["AccessAndBilling"]) @@ -132,7 +133,8 @@ def GetDetail(overwrite=false) # map {} to nil - some weird XML converstion issue? @flatten=@flatten.inject({}) { |sum,a| sum[a[0]]=a[1]=={} ? nil : a[1]; sum } @attributes=@flatten.merge(@attributes) - @attributes.merge!(@flatten.raw) if overwrite + @attributes.merge!(@flatten) if overwrite + @fully_baked=true if @result.success? @result.success? end @@ -194,7 +196,7 @@ def UpdateBasics # @client.Create def Create @result=Result.new(cm_client.Client_Create(@attributes)) - self.id = @result.content + self.id = @result.content if @result.success? @result.success? end From ce09a4a426a949469600db9e85fed5d9a74698eb Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 04:43:16 -0500 Subject: [PATCH 21/31] first step towards new campaign interface --- lib/campaign_monitor.rb | 3 +- lib/campaign_monitor/base.rb | 2 +- lib/campaign_monitor/campaign.rb | 86 +++++++++++++++++++++++--------- lib/campaign_monitor/result.rb | 10 ++-- test/campaign_monitor_test.rb | 2 +- test/client_test.rb | 2 +- test/list_test.rb | 4 +- test/test_helper.rb | 2 + 8 files changed, 76 insertions(+), 35 deletions(-) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 8c0901c..bc1e0ba 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -108,6 +108,7 @@ def request(method, params) 'forcearray' => %w[List Campaign Subscriber Client SubscriberOpen SubscriberUnsubscribe SubscriberClick SubscriberBounce], 'noattr' => true }) response.delete('d1p1:type') + response.delete("d1p1:http://www.w3.org/2001/XMLSchema-instance:type") response rescue XML::Parser::ParseError { "Code" => 500, "Message" => request_xml.split(/\r?\n/).first, "FullError" => request_xml } @@ -190,7 +191,7 @@ def timezones # end def campaigns(client_id) handle_response(Client_GetCampaigns("ClientID" => client_id)) do |response| - response["Campaign"].collect{|c| Campaign.new(c["CampaignID"], c["Subject"], c["SentDate"], c["TotalRecipients"].to_i)} + response["Campaign"].collect{|c| Campaign.new(c) } end end diff --git a/lib/campaign_monitor/base.rb b/lib/campaign_monitor/base.rb index 8f487aa..a69cd85 100644 --- a/lib/campaign_monitor/base.rb +++ b/lib/campaign_monitor/base.rb @@ -2,7 +2,7 @@ class CampaignMonitor # Provides access to the lists and campaigns associated with a client class Base - attr_reader :result + attr_reader :result, :attributes @@client=nil diff --git a/lib/campaign_monitor/campaign.rb b/lib/campaign_monitor/campaign.rb index d1f0c05..4678b14 100644 --- a/lib/campaign_monitor/campaign.rb +++ b/lib/campaign_monitor/campaign.rb @@ -1,16 +1,47 @@ class CampaignMonitor - # Provides access to the information about a campaign - class Campaign + + # The Client class aims to impliment the full functionality of the CampaignMonitor + # Clients API as detailed at: http://www.campaignmonitor.com/api/ + # === Attributes + # + # Attriutes can be read and set as if Campaign were a Hash + # + # @client["CompanyName"]="Road Running, Inc." + # @client["ContactName"] => "Wiley Coyote" + # + # Convenience attribute readers are provided for name and id + # + # @campaign.id == @client["CampaignID"] + # @campaign.name == @client["CampaignName"] + # + # === API calls supported + # + # * Campaign.Create + # * Campaign.Send + # * Campaign.GetBounces + # * Campaign.GetLists + # * Campaign.GetOpens + # * Campaign.GetSubscriberClicks + # * Campaign.GetUnsubscribes + # * Campaign.GetSummary + # + # === Not yet supported + # + # + class Campaign < Base include CampaignMonitor::Helpers + id_field "CampaignID" + name_field "Subject" - attr_reader :id, :subject, :sent_date, :total_recipients, :cm_client +# attr_reader :id, :subject, :sent_date, :total_recipients, :cm_client + + data_types "TotalRecipients" => "to_i" - def initialize(id=nil, subject=nil, sent_date=nil, total_recipients=nil) - @id = id - @subject = subject - @sent_date = sent_date - @total_recipients = total_recipients - @cm_client = CampaignMonitor.new + attr_reader :cm_client + + def initialize(attrs={}) + super + @attributes=attrs end # Example @@ -20,11 +51,12 @@ def initialize(id=nil, subject=nil, sent_date=nil, total_recipients=nil) # for subscriber in @subscriber_opens # puts subscriber.email # end - def opens + def GetOpens handle_response(cm_client.Campaign_GetOpens("CampaignID" => self.id)) do |response| response["SubscriberOpen"].collect{|s| SubscriberOpen.new(s["EmailAddress"], s["ListID"], s["NumberOfOpens"])} end end + alias opens GetOpens # Example # @campaign = Campaign.new(12345) @@ -33,11 +65,12 @@ def opens # for subscriber in @subscriber_bounces # puts subscriber.email # end - def bounces + def GetBounces handle_response(cm_client.Campaign_GetBounces("CampaignID"=> self.id)) do |response| response["SubscriberBounce"].collect{|s| SubscriberBounce.new(s["EmailAddress"], s["ListID"], s["BounceType"])} end end + alias bounces GetBounces # Example # @campaign = Campaign.new(12345) @@ -46,11 +79,12 @@ def bounces # for subscriber in @subscriber_clicks # puts subscriber.email # end - def clicks + def GetSubscriberClicks handle_response(cm_client.Campaign_GetSubscriberClicks("CampaignID" => self.id)) do |response| response["SubscriberClick"].collect{|s| SubscriberClick.new(s["EmailAddress"], s["ListID"], s["ClickedLinks"])} end end + alias clicks GetSubscriberClicks # Example # @campaign = Campaign.new(12345) @@ -59,52 +93,59 @@ def clicks # for subscriber in @subscriber_unsubscribes # puts subscriber.email # end - def unsubscribes + def GetUnsubscribes handle_response(cm_client.Campaign_GetUnsubscribes("CampaignID" => self.id)) do |response| response["SubscriberUnsubscribe"].collect{|s| SubscriberUnsubscribe.new(s["EmailAddress"], s["ListID"])} end end + alias unsubscribes GetUnsubscribes + + def GetSummary + @result=Result.new(cm_client.Campaign_GetSummary('CampaignID' => self.id)) + @summary=@result.raw if @result.success? + @result.success? + end # Example # @campaign = Campaign.new(12345) # puts @campaign.number_recipients def number_recipients - @number_recipients ||= attributes[:number_recipients] + @number_recipients ||= summary[:number_recipients] end # Example # @campaign = Campaign.new(12345) # puts @campaign.number_opened def number_opened - @number_opened ||= attributes[:number_opened] + @number_opened ||= summary[:number_opened] end # Example # @campaign = Campaign.new(12345) # puts @campaign.number_clicks def number_clicks - @number_clicks ||= attributes[:number_clicks] + @number_clicks ||= summary[:number_clicks] end # Example # @campaign = Campaign.new(12345) # puts @campaign.number_unsubscribed def number_unsubscribed - @number_unsubscribed ||= attributes[:number_unsubscribed] + @number_unsubscribed ||= summary[:number_unsubscribed] end # Example # @campaign = Campaign.new(12345) # puts @campaign.number_bounced def number_bounced - @number_bounced ||= attributes[:number_bounced] + @number_bounced ||= summary[:number_bounced] end private - def attributes - if @attributes.nil? + def summary + if @summary.nil? summary = cm_client.Campaign_GetSummary('CampaignID' => self.id) - @attributes = { + @summary = { :number_recipients => summary['Recipients'].to_i, :number_opened => summary['TotalOpened'].to_i, :number_clicks => summary['Click'].to_i, @@ -112,8 +153,7 @@ def attributes :number_bounced => summary['Bounced'].to_i } end - - @attributes + @summary end end end \ No newline at end of file diff --git a/lib/campaign_monitor/result.rb b/lib/campaign_monitor/result.rb index 858afd9..e01ce86 100644 --- a/lib/campaign_monitor/result.rb +++ b/lib/campaign_monitor/result.rb @@ -1,12 +1,12 @@ class CampaignMonitor # Encapsulates the response received from the CampaignMonitor webservice. class Result - attr_reader :message, :code + attr_reader :message, :code, :raw def initialize(response) @message = response["Message"] @code = response["Code"].to_i - @raw=response + @raw = response end def success? @@ -14,7 +14,7 @@ def success? end def failed? - !succeeded? + not success? end def content @@ -24,9 +24,5 @@ def content alias :succeeded? :success? alias :failure? :failed? - def raw - @raw - end - end end \ No newline at end of file diff --git a/test/campaign_monitor_test.rb b/test/campaign_monitor_test.rb index 8b3c276..0ed42c2 100644 --- a/test/campaign_monitor_test.rb +++ b/test/campaign_monitor_test.rb @@ -1,5 +1,5 @@ require 'rubygems' -require 'campaign_monitor' +require 'lib/campaign_monitor' require 'test/unit' require 'test/test_helper' diff --git a/test/client_test.rb b/test/client_test.rb index d2cc241..b9995d3 100644 --- a/test/client_test.rb +++ b/test/client_test.rb @@ -1,5 +1,5 @@ require 'rubygems' -require 'campaign_monitor' +require 'lib/campaign_monitor' require 'test/unit' require 'test/test_helper' diff --git a/test/list_test.rb b/test/list_test.rb index aff5ad7..9c8e800 100644 --- a/test/list_test.rb +++ b/test/list_test.rb @@ -1,5 +1,5 @@ require 'rubygems' -require 'campaign_monitor' +require 'lib/campaign_monitor' require 'test/unit' require 'test/test_helper' @@ -8,6 +8,8 @@ class CampaignMonitorTest < Test::Unit::TestCase + $debug=true + def setup @cm = CampaignMonitor.new(ENV["API_KEY"]) # find an existing client diff --git a/test/test_helper.rb b/test/test_helper.rb index 6d74735..9a49df1 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,5 @@ +CAMPAIGN_MONITOR_API_KEY=nil + if ENV["API_KEY"].nil? puts "Please specify the API_KEY on the command line for testing." exit From e32f9b4d047463f5e1aae7761870f77985936d94 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 07:03:26 -0500 Subject: [PATCH 22/31] support for creating campaigns --- campaign_monitor.gemspec | 3 +- lib/campaign_monitor.rb | 5 +- lib/campaign_monitor/campaign.rb | 95 +++++++++++++++++-------------- lib/campaign_monitor/client.rb | 4 ++ lib/campaign_monitor/result.rb | 3 + test/campaign_test.rb | 98 ++++++++++++++++++++++++++++++++ test/test_helper.rb | 11 ++++ 7 files changed, 175 insertions(+), 44 deletions(-) create mode 100644 test/campaign_test.rb diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index fe489fb..22ff776 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'campaign_monitor' - s.version = "1.3.0" + s.version = "1.3.2" s.summary = 'Provides access to the Campaign Monitor API.' s.description = <<-EOF A simple wrapper class that provides basic access to the Campaign Monitor API. @@ -15,6 +15,7 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.add_dependency 'xml-simple', ['>= 1.0.11'] + s.add_dependency 'soapr4', ['>= 1.5.8'] s.files = [ 'campaign_monitor.gemspec', diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index bc1e0ba..c96311c 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -3,6 +3,7 @@ require 'net/http' require 'xmlsimple' require 'date' +gem 'soap4r' require File.join(File.dirname(__FILE__), '../support/class_enhancements.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/helpers.rb') @@ -110,7 +111,8 @@ def request(method, params) response.delete('d1p1:type') response.delete("d1p1:http://www.w3.org/2001/XMLSchema-instance:type") response - rescue XML::Parser::ParseError + # rescue XML::Parser::ParseError + rescue XML::Error { "Code" => 500, "Message" => request_xml.split(/\r?\n/).first, "FullError" => request_xml } end end @@ -225,6 +227,7 @@ def add_subscriber(list_id, email, name) def using_soap driver = wsdl_driver_factory.create_rpc_driver + driver.wiredump_dev = STDERR if $debug response = yield(driver) driver.reset_stream diff --git a/lib/campaign_monitor/campaign.rb b/lib/campaign_monitor/campaign.rb index 4678b14..f4372b9 100644 --- a/lib/campaign_monitor/campaign.rb +++ b/lib/campaign_monitor/campaign.rb @@ -33,6 +33,9 @@ class Campaign < Base id_field "CampaignID" name_field "Subject" + class MissingParameter < StandardError + end + # attr_reader :id, :subject, :sent_date, :total_recipients, :cm_client data_types "TotalRecipients" => "to_i" @@ -44,6 +47,35 @@ def initialize(attrs={}) @attributes=attrs end + def Create + required_params=%w{CampaignName CampaignSubject FromName FromEmail ReplyTo HtmlUrl TextUrl} + required_params.each do |f| + raise MissingParameter, "'#{f}' is required to call Create" unless self[f] + end + response = cm_client.using_soap do |driver| + opts=attributes.merge(:ApiKey => cm_client.api_key, :SubscriberListIDs => @lists.map {|x| x.id}) + driver.createCampaign opts + end + @result=Result.new(response["Campaign.CreateResult"]) + self.id=@result.content if @result.success? + @result.success? + end + + def Send(options={}) + required_params=%w{ConfirmationEmail SendDate} + required_params.each do |f| + raise MissingParameter, "'#{f}' is required to call Send" unless options[f] + end + options.merge!("CampaignID" => self.id) + @result=Result.new(@cm_client.Campaign_Send(options)) + @result.success? + end + + def add_list(list) + @lists||=[] + @lists << list + end + # Example # @campaign = Campaign.new(12345) # @subscriber_opens = @campaign.opens @@ -102,56 +134,35 @@ def GetUnsubscribes def GetSummary @result=Result.new(cm_client.Campaign_GetSummary('CampaignID' => self.id)) - @summary=@result.raw if @result.success? + @summary=parse_summary(@result.raw) if @result.success? @result.success? end - # Example - # @campaign = Campaign.new(12345) - # puts @campaign.number_recipients - def number_recipients - @number_recipients ||= summary[:number_recipients] - end - - # Example - # @campaign = Campaign.new(12345) - # puts @campaign.number_opened - def number_opened - @number_opened ||= summary[:number_opened] - end - - # Example - # @campaign = Campaign.new(12345) - # puts @campaign.number_clicks - def number_clicks - @number_clicks ||= summary[:number_clicks] - end - - # Example - # @campaign = Campaign.new(12345) - # puts @campaign.number_unsubscribed - def number_unsubscribed - @number_unsubscribed ||= summary[:number_unsubscribed] + # hook up the old API calls + def method_missing(m, *args) + if %w{number_bounced number_unsubscribed number_clicks number_opened number_recipients}.include?(m.to_s) + summary[m] + else + super + end end - # Example - # @campaign = Campaign.new(12345) - # puts @campaign.number_bounced - def number_bounced - @number_bounced ||= summary[:number_bounced] + def summary(refresh=false) + self.GetSummary if refresh or @summary.nil? + @summary end private - def summary - if @summary.nil? - summary = cm_client.Campaign_GetSummary('CampaignID' => self.id) - @summary = { - :number_recipients => summary['Recipients'].to_i, - :number_opened => summary['TotalOpened'].to_i, - :number_clicks => summary['Click'].to_i, - :number_unsubscribed => summary['Unsubscribed'].to_i, - :number_bounced => summary['Bounced'].to_i - } + def parse_summary(summary) + @summary = { + :number_recipients => summary['Recipients'].to_i, + :number_opened => summary['TotalOpened'].to_i, + :number_clicks => summary['Clicks'].to_i, + :number_unsubscribed => summary['Unsubscribed'].to_i, + :number_bounced => summary['Bounced'].to_i + } + summary.each do |key, value| + @summary[key]=value.to_i end @summary end diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index 2ddf261..3e03b46 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -96,6 +96,10 @@ def GetCampaigns alias campaigns GetCampaigns + def new_campaign(attrs={}) + Campaign.new(attrs.merge("ClientID" => self.id)) + end + # Calls Client.GetDetails to load a specific client # Client#result will have the result of the API call diff --git a/lib/campaign_monitor/result.rb b/lib/campaign_monitor/result.rb index e01ce86..3a3acb0 100644 --- a/lib/campaign_monitor/result.rb +++ b/lib/campaign_monitor/result.rb @@ -18,6 +18,9 @@ def failed? end def content + # if we're a string (likely from SOAP) + return raw if raw.is_a?(String) + # if we're a hash raw["__content__"] end diff --git a/test/campaign_test.rb b/test/campaign_test.rb new file mode 100644 index 0000000..4e5acbd --- /dev/null +++ b/test/campaign_test.rb @@ -0,0 +1,98 @@ +require 'rubygems' +require 'lib/campaign_monitor' +require 'test/unit' +require 'test/test_helper' + +CLIENT_NAME = 'Spacely Space Sprockets' +CLIENT_CONTACT_NAME = 'George Jetson' +LIST_NAME = 'List #1' + +class CampaignMonitorTest < Test::Unit::TestCase + + def setup + @cm = CampaignMonitor.new(ENV["API_KEY"]) + # find an existing client and make sure we know it's values + @client=find_test_client(@cm.clients) + assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." + + # delete all existing lists + @client.lists.each { |l| l.Delete } + @list = @client.lists.build.defaults + end + + + def teardown + end + + def test_finds_named_campaign + @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } + assert_not_nil @campaign + assert_equal 1, @campaign["TotalRecipients"] + end + + def test_summary_interface + @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } + assert_not_nil @campaign + # old + assert_equal 1, @campaign.number_recipients + assert_equal 0, @campaign.number_opened + assert_equal 0, @campaign.number_clicks + assert_equal 0, @campaign.number_unsubscribed + assert_equal 0, @campaign.number_bounced + # new + assert_equal 1, @campaign.summary["Recipients"] + assert_equal 0, @campaign.summary["TotalOpened"] + assert_equal 0, @campaign.summary["Clicks"] + assert_equal 0, @campaign.summary["Unsubscribed"] + assert_equal 0, @campaign.summary["Bounced"] + end + + def test_creating_a_campaign + return + @campaign=@client.new_campaign + # create two lists + @beef=@client.lists.build.defaults + @beef["Title"]="Beef" + @beef.Create + assert_success @beef.result + @chicken=@client.lists.build.defaults + @chicken["Title"]="Chicken" + @chicken.Create + assert_success @chicken.result + + @campaign.add_list @beef + @campaign.add_list @chicken + @campaign["CampaignName"]="Noodles #{secure_digest(Time.now.to_s)}" + @campaign["CampaignSubject"]="Noodly #{secure_digest(Time.now.to_s)}" + puts @campaign.inspect + @campaign["FromName"] = "George Bush" + @campaign["FromEmail"] = "george@aol.com" + @campaign["ReplyTo"] = "george@aol.com" + @campaign["HtmlUrl"] = "http://www.google.com/robots.txt" + @campaign["TextUrl"] = "http://www.google.com/robots.txt" + @campaign.Create + puts @campaign.result.inspect + assert_success @campaign.result + assert_not_nil @campaign.id + assert_equal 32, @campaign.id.length + # test sending + @campaign.Send("ConfirmationEmail" => "george@aol.com", "SendDate" => "Immediately") + assert_success @campaign.result + + end + + def test_GetSummary + @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } + assert_not_nil @campaign + @campaign.GetSummary + assert @campaign.result.success? + end + + + protected + + def find_test_client(clients) + clients.detect { |c| c.name == CLIENT_NAME } + end + +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 9a49df1..53f1fc6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,3 +1,6 @@ +require 'ruby-debug' +require 'digest' + CAMPAIGN_MONITOR_API_KEY=nil if ENV["API_KEY"].nil? @@ -13,4 +16,12 @@ def assert_not_empty(v) assert !v.strip.empty?, "expected to not be empty" if v.is_a?(String) end + def assert_success(result) + assert result.succeeded?, "#{result.code}: #{result.message}" + end + + def secure_digest(*args) + Digest::SHA1.hexdigest(args.flatten.join('--')) + end + end \ No newline at end of file From 2c8eee69f8a2ec777c184dbd391ba750c9feb006 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 07:26:03 -0500 Subject: [PATCH 23/31] make lists and array and rip out misc classes into their own file --- TODO | 6 +++++ campaign_monitor.gemspec | 3 +++ lib/campaign_monitor.rb | 44 +----------------------------- lib/campaign_monitor/campaign.rb | 17 ++++++++---- lib/campaign_monitor/misc.rb | 46 ++++++++++++++++++++++++++++++++ test/campaign_test.rb | 10 ++++--- 6 files changed, 75 insertions(+), 51 deletions(-) create mode 100644 TODO create mode 100644 lib/campaign_monitor/misc.rb diff --git a/TODO b/TODO new file mode 100644 index 0000000..80ca16f --- /dev/null +++ b/TODO @@ -0,0 +1,6 @@ +* Decide on convents about when to raise and when to return true/false + + So far I've kind of taken the stance if we're retrieving a list and there + are only two possible failures (APIKey or primary key) to raise if there + is an error but for update/save/create operations to return true/false + and then let the user probe @object.result for more details \ No newline at end of file diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index 22ff776..738224b 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -24,9 +24,11 @@ Gem::Specification.new do |s| 'MIT-LICENSE', 'Rakefile', 'README.rdoc', + 'TODO', 'lib/campaign_monitor.rb', 'lib/campaign_monitor/base.rb', + 'lib/campaign_monitor/misc.rb', 'lib/campaign_monitor/campaign.rb', 'lib/campaign_monitor/client.rb', 'lib/campaign_monitor/helpers.rb', @@ -41,6 +43,7 @@ Gem::Specification.new do |s| 'support/faster-xml-simple/test/xml_simple_comparison_test.rb', 'test/campaign_monitor_test.rb', + 'test/campaign_test.rb', 'test/client_test.rb', 'test/list_test.rb', 'test/test_helper.rb' diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index c96311c..2422ce6 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -7,6 +7,7 @@ require File.join(File.dirname(__FILE__), '../support/class_enhancements.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/helpers.rb') +require File.join(File.dirname(__FILE__), 'campaign_monitor/misc.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/base.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/client.rb') require File.join(File.dirname(__FILE__), 'campaign_monitor/list.rb') @@ -234,49 +235,6 @@ def using_soap response end - # Encapsulates - class SubscriberBounce #:nodoc: - attr_reader :email_address, :bounce_type, :list_id - - def initialize(email_address, list_id, bounce_type) - @email_address = email_address - @bounce_type = bounce_type - @list_id = list_id - end - end - - # Encapsulates - class SubscriberOpen #:nodoc: - attr_reader :email_address, :list_id, :opens - - def initialize(email_address, list_id, opens) - @email_address = email_address - @list_id = list_id - @opens = opens - end - end - - # Encapsulates - class SubscriberClick #:nodoc: - attr_reader :email_address, :list_id, :clicked_links - - def initialize(email_address, list_id, clicked_links) - @email_address = email_address - @list_id = list_id - @clicked_links = clicked_links - end - end - - # Encapsulates - class SubscriberUnsubscribe #:nodoc: - attr_reader :email_address, :list_id - - def initialize(email_address, list_id) - @email_address = email_address - @list_id = list_id - end - end - protected def wsdl_driver_factory diff --git a/lib/campaign_monitor/campaign.rb b/lib/campaign_monitor/campaign.rb index f4372b9..f3742cf 100644 --- a/lib/campaign_monitor/campaign.rb +++ b/lib/campaign_monitor/campaign.rb @@ -24,9 +24,6 @@ class CampaignMonitor # * Campaign.GetSubscriberClicks # * Campaign.GetUnsubscribes # * Campaign.GetSummary - # - # === Not yet supported - # # class Campaign < Base include CampaignMonitor::Helpers @@ -71,9 +68,19 @@ def Send(options={}) @result.success? end - def add_list(list) + def GetLists + handle_response(@cm_client.Campaign_GetLists(:CampaignID => id)) do |response| + @result=Result.new(response) + if @result.success? + @lists=response["List"].collect{|l| List.new({"ListID" => l["ListID"], "Title" => l["Name"]})} + end + end + end + + def lists + # pull down the list of lists if we have an id + self.GetLists if @lists.nil? and id @lists||=[] - @lists << list end # Example diff --git a/lib/campaign_monitor/misc.rb b/lib/campaign_monitor/misc.rb new file mode 100644 index 0000000..70fd911 --- /dev/null +++ b/lib/campaign_monitor/misc.rb @@ -0,0 +1,46 @@ +class CampaignMonitor + + # Encapsulates + class SubscriberBounce #:nodoc: + attr_reader :email_address, :bounce_type, :list_id + + def initialize(email_address, list_id, bounce_type) + @email_address = email_address + @bounce_type = bounce_type + @list_id = list_id + end + end + + # Encapsulates + class SubscriberOpen #:nodoc: + attr_reader :email_address, :list_id, :opens + + def initialize(email_address, list_id, opens) + @email_address = email_address + @list_id = list_id + @opens = opens + end + end + + # Encapsulates + class SubscriberClick #:nodoc: + attr_reader :email_address, :list_id, :clicked_links + + def initialize(email_address, list_id, clicked_links) + @email_address = email_address + @list_id = list_id + @clicked_links = clicked_links + end + end + + # Encapsulates + class SubscriberUnsubscribe #:nodoc: + attr_reader :email_address, :list_id + + def initialize(email_address, list_id) + @email_address = email_address + @list_id = list_id + end + end + +end \ No newline at end of file diff --git a/test/campaign_test.rb b/test/campaign_test.rb index 4e5acbd..d58b15d 100644 --- a/test/campaign_test.rb +++ b/test/campaign_test.rb @@ -28,6 +28,8 @@ def test_finds_named_campaign @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } assert_not_nil @campaign assert_equal 1, @campaign["TotalRecipients"] + assert_equal 1, @campaign.lists.size + assert_equal Hash.new, @campaign.lists.first["Title"] end def test_summary_interface @@ -60,8 +62,8 @@ def test_creating_a_campaign @chicken.Create assert_success @chicken.result - @campaign.add_list @beef - @campaign.add_list @chicken + @campaign.lists << @beef + @campaign.lists << @chicken @campaign["CampaignName"]="Noodles #{secure_digest(Time.now.to_s)}" @campaign["CampaignSubject"]="Noodly #{secure_digest(Time.now.to_s)}" puts @campaign.inspect @@ -75,10 +77,12 @@ def test_creating_a_campaign assert_success @campaign.result assert_not_nil @campaign.id assert_equal 32, @campaign.id.length + # test GetLists + @campaign.instance_variable_set("@lists",nil) + assert_equal 2, @campaign.lists.size # test sending @campaign.Send("ConfirmationEmail" => "george@aol.com", "SendDate" => "Immediately") assert_success @campaign.result - end def test_GetSummary From 621a69e0b9b225d664289809091799dc66d1aaff Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 07:36:26 -0500 Subject: [PATCH 24/31] pull cm_client into base --- lib/campaign_monitor/base.rb | 2 +- lib/campaign_monitor/campaign.rb | 4 ---- lib/campaign_monitor/client.rb | 2 -- lib/campaign_monitor/list.rb | 2 -- lib/campaign_monitor/subscriber.rb | 1 - test/campaign_test.rb | 7 +++---- 6 files changed, 4 insertions(+), 14 deletions(-) diff --git a/lib/campaign_monitor/base.rb b/lib/campaign_monitor/base.rb index a69cd85..59d5ebe 100644 --- a/lib/campaign_monitor/base.rb +++ b/lib/campaign_monitor/base.rb @@ -2,7 +2,7 @@ class CampaignMonitor # Provides access to the lists and campaigns associated with a client class Base - attr_reader :result, :attributes + attr_reader :result, :attributes, :cm_client @@client=nil diff --git a/lib/campaign_monitor/campaign.rb b/lib/campaign_monitor/campaign.rb index f3742cf..1846a5b 100644 --- a/lib/campaign_monitor/campaign.rb +++ b/lib/campaign_monitor/campaign.rb @@ -33,12 +33,8 @@ class Campaign < Base class MissingParameter < StandardError end -# attr_reader :id, :subject, :sent_date, :total_recipients, :cm_client - data_types "TotalRecipients" => "to_i" - attr_reader :cm_client - def initialize(attrs={}) super @attributes=attrs diff --git a/lib/campaign_monitor/client.rb b/lib/campaign_monitor/client.rb index 3e03b46..71f78cd 100644 --- a/lib/campaign_monitor/client.rb +++ b/lib/campaign_monitor/client.rb @@ -48,8 +48,6 @@ class Client < Base # we will assume if something isn't a basic attribute that it's a AccessAndBilling attribute BASIC_ATTRIBUTES=%w{CompanyName ContactName EmailAddress Country Timezone} - attr_reader :cm_client - # Creates a new client that you can later create (or load) # The prefered way to load a client is using Client#[] however # diff --git a/lib/campaign_monitor/list.rb b/lib/campaign_monitor/list.rb index 0267cba..656e47b 100644 --- a/lib/campaign_monitor/list.rb +++ b/lib/campaign_monitor/list.rb @@ -6,8 +6,6 @@ class CampaignMonitor class List < Base include CampaignMonitor::Helpers - attr_reader :cm_client - id_field "ListID" name_field "Title" diff --git a/lib/campaign_monitor/subscriber.rb b/lib/campaign_monitor/subscriber.rb index de4ca00..49b42ed 100644 --- a/lib/campaign_monitor/subscriber.rb +++ b/lib/campaign_monitor/subscriber.rb @@ -4,7 +4,6 @@ class Subscriber < Base include CampaignMonitor::Helpers attr_accessor :email_address, :name, :date_subscribed - attr_reader :cm_client def initialize(email_address, name=nil, date=nil) @email_address = email_address diff --git a/test/campaign_test.rb b/test/campaign_test.rb index d58b15d..82d6a6d 100644 --- a/test/campaign_test.rb +++ b/test/campaign_test.rb @@ -14,19 +14,19 @@ def setup # find an existing client and make sure we know it's values @client=find_test_client(@cm.clients) assert_not_nil @client, "Please create a '#{CLIENT_NAME}' (company name) client so tests can run." + + @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } + assert_not_nil @campaign, "Please create a campaign named 'Big Deal' so tests can run." # delete all existing lists @client.lists.each { |l| l.Delete } @list = @client.lists.build.defaults end - def teardown end def test_finds_named_campaign - @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } - assert_not_nil @campaign assert_equal 1, @campaign["TotalRecipients"] assert_equal 1, @campaign.lists.size assert_equal Hash.new, @campaign.lists.first["Title"] @@ -50,7 +50,6 @@ def test_summary_interface end def test_creating_a_campaign - return @campaign=@client.new_campaign # create two lists @beef=@client.lists.build.defaults From 8938c393fd3299a62b1b5c70d46ee48179df5254 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 08:19:50 -0500 Subject: [PATCH 25/31] update docs, tests, add ApiError --- lib/campaign_monitor.rb | 3 ++ lib/campaign_monitor/campaign.rb | 91 +++++++++++++++++++++++++++----- lib/campaign_monitor/helpers.rb | 2 +- test/campaign_test.rb | 14 +++++ 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/lib/campaign_monitor.rb b/lib/campaign_monitor.rb index 2422ce6..997dfd2 100644 --- a/lib/campaign_monitor.rb +++ b/lib/campaign_monitor.rb @@ -91,6 +91,9 @@ class CampaignMonitor class InvalidAPIKey < StandardError end + + class ApiError < StandardError + end attr_reader :api_key, :api_url diff --git a/lib/campaign_monitor/campaign.rb b/lib/campaign_monitor/campaign.rb index 1846a5b..040720e 100644 --- a/lib/campaign_monitor/campaign.rb +++ b/lib/campaign_monitor/campaign.rb @@ -40,6 +40,20 @@ def initialize(attrs={}) @attributes=attrs end + # Calls Campaign.Create + # It will return true if successful and false if not. + # Campaign#result will have the result of the API call + # + # Example + # @camp=@client.new_campaign + # @camp["CampaignName"]="Yummy Gummy Bears" + # @camp["CampaignSubject"]="Yummy Gummy Bears" + # @camp["FromName"]="Mr Yummy" + # @camp["FromEmail"]="yummy@gummybears.com" + # @camp["ReplyTo"]="support@gummybears.com" + # @camp["HtmlUrl"]="http://www.gummybears.com/newsletter2009.html" + # @camp["TextUrl"]="http://www.gummybears.com/newsletter2009.txt" + # @camp.Create def Create required_params=%w{CampaignName CampaignSubject FromName FromEmail ReplyTo HtmlUrl TextUrl} required_params.each do |f| @@ -54,6 +68,14 @@ def Create @result.success? end + # Calls Campaign.Send + # It will return true if successful and false if not. + # Campaign#result will have the result of the API call + # + # Example + # @camp=@client.new_campaign(attributes) + # @camp.Create + # @camp.Send("ConfirmationEmail" => "bob@aol.com", "SendDate" => "Immediately") def Send(options={}) required_params=%w{ConfirmationEmail SendDate} required_params.each do |f| @@ -64,15 +86,42 @@ def Send(options={}) @result.success? end + # Calls Campaign.GetLists. Often you probably should just use Campaign#lists + # It will raise an ApiError if an error occurs + # Campaign#result will have the result of the API call + # + # Example + # @camp=@client.campaigns.first + # @camp.GetLists def GetLists handle_response(@cm_client.Campaign_GetLists(:CampaignID => id)) do |response| @result=Result.new(response) - if @result.success? - @lists=response["List"].collect{|l| List.new({"ListID" => l["ListID"], "Title" => l["Name"]})} - end + @lists=response["List"].collect{|l| List.new({"ListID" => l["ListID"], "Title" => l["Name"]})} end end + # Creates a new list object with the given id. + # You'll still need to call another method to load data or actually do anything useful + # as this method just generators a new object and doesn't hit the API at all. This was + # added as a quick way to setup an object to request data from it + # + # Example + # @campaign=Campaign[1234] + # @campaign.lists.each do ... + def self.[](id) + Campaign.new("CampaignID" => id) + end + + # Convenience method for accessing or adding lists to a new (uncreated) campaign + # Calls GetLists behind the scenes if needed + # + # Example + # @camp=@client.campaigns.first + # @camp.lists.each do + # + # @camp=@client.new_campaign(attributes) + # @camp.lists << @client.lists.first + # @camp.Create def lists # pull down the list of lists if we have an id self.GetLists if @lists.nil? and id @@ -80,7 +129,7 @@ def lists end # Example - # @campaign = Campaign.new(12345) + # @campaign = Campaign[12345] # @subscriber_opens = @campaign.opens # # for subscriber in @subscriber_opens @@ -94,7 +143,7 @@ def GetOpens alias opens GetOpens # Example - # @campaign = Campaign.new(12345) + # @campaign = Campaign[12345] # @subscriber_bounces = @campaign.bounces # # for subscriber in @subscriber_bounces @@ -108,7 +157,7 @@ def GetBounces alias bounces GetBounces # Example - # @campaign = Campaign.new(12345) + # @campaign = Campaign[12345] # @subscriber_clicks = @campaign.clicks # # for subscriber in @subscriber_clicks @@ -122,7 +171,7 @@ def GetSubscriberClicks alias clicks GetSubscriberClicks # Example - # @campaign = Campaign.new(12345) + # @campaign = Campaign[12345] # @subscriber_unsubscribes = @campaign.unsubscribes # # for subscriber in @subscriber_unsubscribes @@ -135,12 +184,6 @@ def GetUnsubscribes end alias unsubscribes GetUnsubscribes - def GetSummary - @result=Result.new(cm_client.Campaign_GetSummary('CampaignID' => self.id)) - @summary=parse_summary(@result.raw) if @result.success? - @result.success? - end - # hook up the old API calls def method_missing(m, *args) if %w{number_bounced number_unsubscribed number_clicks number_opened number_recipients}.include?(m.to_s) @@ -150,6 +193,28 @@ def method_missing(m, *args) end end + # Calls Campaign.GetSummary. Often you probably should just use Campaign#summary + # It will return true if successful and false if not. + # Campaign#result will have the result of the API call + # + # Example + # @camp=@client.campaigns.first + # @camp.GetSummary + def GetSummary + @result=Result.new(cm_client.Campaign_GetSummary('CampaignID' => self.id)) + @summary=parse_summary(@result.raw) if @result.success? + @result.success? + end + + # Convenience method for accessing summary details of a campaign + # + # Examples + # @camp.summary["Recipients"] + # @camp.summary['Recipients'] + # @camp.summary['TotalOpened'] + # @camp.summary['Clicks'] + # @camp.summary['Unsubscribed'] + # @camp.summary['Bounced'] def summary(refresh=false) self.GetSummary if refresh or @summary.nil? @summary diff --git a/lib/campaign_monitor/helpers.rb b/lib/campaign_monitor/helpers.rb index ccdf4b3..c5904c2 100644 --- a/lib/campaign_monitor/helpers.rb +++ b/lib/campaign_monitor/helpers.rb @@ -11,7 +11,7 @@ def handle_response(response) raise InvalidAPIKey else # error! - raise response["Code"] + " - " + response["Message"] + raise ApiError, response["Code"] + ": " + response["Message"] end end diff --git a/test/campaign_test.rb b/test/campaign_test.rb index 82d6a6d..9427c11 100644 --- a/test/campaign_test.rb +++ b/test/campaign_test.rb @@ -32,6 +32,19 @@ def test_finds_named_campaign assert_equal Hash.new, @campaign.lists.first["Title"] end + def test_bracket_lookup_for_nonexistant + @campaign=CampaignMonitor::Campaign[12345] + assert_not_nil @campaign + assert_equal 12345, @campaign.id + assert_raises( CampaignMonitor::ApiError ) { @campaign.lists } + end + + def test_braket_lookup_for_existing + camp=CampaignMonitor::Campaign[@campaign.id] + assert_not_nil camp + camp.lists + end + def test_summary_interface @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } assert_not_nil @campaign @@ -50,6 +63,7 @@ def test_summary_interface end def test_creating_a_campaign + return @campaign=@client.new_campaign # create two lists @beef=@client.lists.build.defaults From c24826ca23a3869423bc99a60dc4a34a67d77fc6 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 08:26:35 -0500 Subject: [PATCH 26/31] GetSummary should raise --- lib/campaign_monitor/campaign.rb | 14 ++++++++------ test/campaign_test.rb | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/campaign_monitor/campaign.rb b/lib/campaign_monitor/campaign.rb index 040720e..f56c480 100644 --- a/lib/campaign_monitor/campaign.rb +++ b/lib/campaign_monitor/campaign.rb @@ -193,17 +193,19 @@ def method_missing(m, *args) end end - # Calls Campaign.GetSummary. Often you probably should just use Campaign#summary - # It will return true if successful and false if not. + # Calls Campaign.GetSummary. OYou probably should just use Campaign#summary which caches results + # It will raise ApiError if an error occurs # Campaign#result will have the result of the API call # # Example # @camp=@client.campaigns.first - # @camp.GetSummary + # @camp.GetSummary["Clicks"] def GetSummary - @result=Result.new(cm_client.Campaign_GetSummary('CampaignID' => self.id)) - @summary=parse_summary(@result.raw) if @result.success? - @result.success? + handle_response(cm_client.Campaign_GetSummary('CampaignID' => self.id)) do |response| + @result=Result.new(response) + @summary=parse_summary(@result.raw) + end + @summary end # Convenience method for accessing summary details of a campaign diff --git a/test/campaign_test.rb b/test/campaign_test.rb index 9427c11..fb87eb2 100644 --- a/test/campaign_test.rb +++ b/test/campaign_test.rb @@ -37,9 +37,10 @@ def test_bracket_lookup_for_nonexistant assert_not_nil @campaign assert_equal 12345, @campaign.id assert_raises( CampaignMonitor::ApiError ) { @campaign.lists } + assert_raises( CampaignMonitor::ApiError ) { @campaign.GetSummary } end - def test_braket_lookup_for_existing + def test_bracket_lookup_for_existing camp=CampaignMonitor::Campaign[@campaign.id] assert_not_nil camp camp.lists From 078ff30ca6fadee713458dfe98c3bdabb0a83ce2 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 08:27:47 -0500 Subject: [PATCH 27/31] version bump to get a new build --- campaign_monitor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index 738224b..3e26dd9 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'campaign_monitor' - s.version = "1.3.2" + s.version = "1.3.2.1" s.summary = 'Provides access to the Campaign Monitor API.' s.description = <<-EOF A simple wrapper class that provides basic access to the Campaign Monitor API. From c26d8cb9da0389113357878a118a15ce55b31646 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 08:32:00 -0500 Subject: [PATCH 28/31] remove dup lines and puts --- test/campaign_test.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/campaign_test.rb b/test/campaign_test.rb index fb87eb2..603fcbe 100644 --- a/test/campaign_test.rb +++ b/test/campaign_test.rb @@ -47,8 +47,6 @@ def test_bracket_lookup_for_existing end def test_summary_interface - @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } - assert_not_nil @campaign # old assert_equal 1, @campaign.number_recipients assert_equal 0, @campaign.number_opened @@ -80,14 +78,12 @@ def test_creating_a_campaign @campaign.lists << @chicken @campaign["CampaignName"]="Noodles #{secure_digest(Time.now.to_s)}" @campaign["CampaignSubject"]="Noodly #{secure_digest(Time.now.to_s)}" - puts @campaign.inspect @campaign["FromName"] = "George Bush" @campaign["FromEmail"] = "george@aol.com" @campaign["ReplyTo"] = "george@aol.com" @campaign["HtmlUrl"] = "http://www.google.com/robots.txt" @campaign["TextUrl"] = "http://www.google.com/robots.txt" @campaign.Create - puts @campaign.result.inspect assert_success @campaign.result assert_not_nil @campaign.id assert_equal 32, @campaign.id.length @@ -100,8 +96,6 @@ def test_creating_a_campaign end def test_GetSummary - @campaign=@client.campaigns.detect { |x| x["Subject"] == "Big Deal" } - assert_not_nil @campaign @campaign.GetSummary assert @campaign.result.success? end From 0ce2a39db0a53b80c01226d4bc0ebba820dc6199 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Thu, 26 Feb 2009 08:45:30 -0500 Subject: [PATCH 29/31] fix dependency typo --- campaign_monitor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index 3e26dd9..a6b95df 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.add_dependency 'xml-simple', ['>= 1.0.11'] - s.add_dependency 'soapr4', ['>= 1.5.8'] + s.add_dependency 'soap4r', ['>= 1.5.8'] s.files = [ 'campaign_monitor.gemspec', From cbb54a2f9fc550c9786424fda14b2f9c1c95e12f Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Mon, 2 Mar 2009 08:37:01 -0500 Subject: [PATCH 30/31] update gemspec --- campaign_monitor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index a6b95df..31a6b31 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.add_dependency 'xml-simple', ['>= 1.0.11'] s.add_dependency 'soap4r', ['>= 1.5.8'] - + s.files = [ 'campaign_monitor.gemspec', 'init.rb', From a26de1d19a34c7da4561c7d17c4dee048795c788 Mon Sep 17 00:00:00 2001 From: Josh Goebel Date: Fri, 6 Mar 2009 09:10:33 -0500 Subject: [PATCH 31/31] try and get github to build the gem --- campaign_monitor.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/campaign_monitor.gemspec b/campaign_monitor.gemspec index 31a6b31..ba757c3 100644 --- a/campaign_monitor.gemspec +++ b/campaign_monitor.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'campaign_monitor' - s.version = "1.3.2.1" + s.version = "1.3.2.2" s.summary = 'Provides access to the Campaign Monitor API.' s.description = <<-EOF A simple wrapper class that provides basic access to the Campaign Monitor API.