createsend 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +3 -0
  3. data/README.md +3 -0
  4. data/Rakefile +23 -0
  5. data/config.example.yaml +2 -0
  6. data/createsend.gemspec +26 -0
  7. data/lib/campaign.rb +90 -0
  8. data/lib/client.rb +117 -0
  9. data/lib/createsend.rb +103 -0
  10. data/lib/list.rb +99 -0
  11. data/lib/subscriber.rb +49 -0
  12. data/lib/template.rb +38 -0
  13. data/test/campaign_test.rb +98 -0
  14. data/test/client_test.rb +107 -0
  15. data/test/createsend_test.rb +96 -0
  16. data/test/fixtures/active_subscribers.json +67 -0
  17. data/test/fixtures/add_subscriber.json +1 -0
  18. data/test/fixtures/apikey.json +3 -0
  19. data/test/fixtures/bounced_subscribers.json +9 -0
  20. data/test/fixtures/campaign_bounces.json +16 -0
  21. data/test/fixtures/campaign_clicks.json +23 -0
  22. data/test/fixtures/campaign_lists.json +10 -0
  23. data/test/fixtures/campaign_opens.json +32 -0
  24. data/test/fixtures/campaign_summary.json +9 -0
  25. data/test/fixtures/campaign_unsubscribes.json +9 -0
  26. data/test/fixtures/campaigns.json +18 -0
  27. data/test/fixtures/client_details.json +25 -0
  28. data/test/fixtures/clients.json +10 -0
  29. data/test/fixtures/countries.json +247 -0
  30. data/test/fixtures/create_campaign.json +1 -0
  31. data/test/fixtures/create_client.json +1 -0
  32. data/test/fixtures/create_custom_field.json +1 -0
  33. data/test/fixtures/create_list.json +1 -0
  34. data/test/fixtures/create_template.json +1 -0
  35. data/test/fixtures/custom_api_error.json +4 -0
  36. data/test/fixtures/custom_fields.json +20 -0
  37. data/test/fixtures/drafts.json +14 -0
  38. data/test/fixtures/import_subscribers.json +7 -0
  39. data/test/fixtures/list_details.json +7 -0
  40. data/test/fixtures/list_stats.json +26 -0
  41. data/test/fixtures/lists.json +10 -0
  42. data/test/fixtures/segments.json +10 -0
  43. data/test/fixtures/subscriber_details.json +20 -0
  44. data/test/fixtures/subscriber_history.json +45 -0
  45. data/test/fixtures/suppressionlist.json +12 -0
  46. data/test/fixtures/systemdate.json +3 -0
  47. data/test/fixtures/template_details.json +6 -0
  48. data/test/fixtures/templates.json +14 -0
  49. data/test/fixtures/timezones.json +99 -0
  50. data/test/fixtures/unsubscribed_subscribers.json +21 -0
  51. data/test/helper.rb +36 -0
  52. data/test/list_test.rb +107 -0
  53. data/test/subscriber_test.rb +73 -0
  54. data/test/template_test.rb +38 -0
  55. metadata +256 -0
@@ -0,0 +1 @@
1
+ "98y2e98y289dh89h938389"
@@ -0,0 +1,4 @@
1
+ {
2
+ "Code": 98798,
3
+ "Message": "A crazy API error"
4
+ }
@@ -0,0 +1,20 @@
1
+ [
2
+ {
3
+ "FieldName": "website",
4
+ "Key": "[website]",
5
+ "DataType": "Text",
6
+ "FieldOptions": []
7
+ },
8
+ {
9
+ "FieldName": "age",
10
+ "Key": "[age]",
11
+ "DataType": "Number",
12
+ "FieldOptions": []
13
+ },
14
+ {
15
+ "FieldName": "subscription date",
16
+ "Key": "[subscriptiondate]",
17
+ "DataType": "Date",
18
+ "FieldOptions": []
19
+ }
20
+ ]
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "CampaignID": "7c7424792065d92627139208c8c01db1",
4
+ "Name": "Draft One",
5
+ "Subject": "Draft One",
6
+ "DateCreated": "2010-08-19 16:08:00"
7
+ },
8
+ {
9
+ "CampaignID": "2e928e982065d92627139208c8c01db1",
10
+ "Name": "Draft Two",
11
+ "Subject": "Draft Two",
12
+ "DateCreated": "2010-08-19 16:08:00"
13
+ }
14
+ ]
@@ -0,0 +1,7 @@
1
+ {
2
+ "FailureDetails": [],
3
+ "TotalUniqueEmailsSubmitted": 3,
4
+ "TotalExistingSubscribers": 0,
5
+ "TotalNewSubscribers": 3,
6
+ "DuplicateEmailsInSubmission": []
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "ConfirmedOptIn": false,
3
+ "Title": "a non-basic list :)",
4
+ "UnsubscribePage": "",
5
+ "ListID": "2fe4c8f0373ce320e2200596d7ef168f",
6
+ "ConfirmationSuccessPage": ""
7
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "TotalActiveSubscribers": 6,
3
+ "NewActiveSubscribersToday": 0,
4
+ "NewActiveSubscribersYesterday": 8,
5
+ "NewActiveSubscribersThisWeek": 8,
6
+ "NewActiveSubscribersThisMonth": 8,
7
+ "NewActiveSubscribersThisYear": 8,
8
+ "TotalUnsubscribes": 2,
9
+ "UnsubscribesToday": 0,
10
+ "UnsubscribesYesterday": 2,
11
+ "UnsubscribesThisWeek": 2,
12
+ "UnsubscribesThisMonth": 2,
13
+ "UnsubscribesThisYear": 2,
14
+ "TotalDeleted": 0,
15
+ "DeletedToday": 0,
16
+ "DeletedYesterday": 0,
17
+ "DeletedThisWeek": 0,
18
+ "DeletedThisMonth": 0,
19
+ "DeletedThisYear": 0,
20
+ "TotalBounces": 0,
21
+ "BouncesToday": 0,
22
+ "BouncesYesterday": 0,
23
+ "BouncesThisWeek": 0,
24
+ "BouncesThisMonth": 0,
25
+ "BouncesThisYear": 0
26
+ }
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "ListID": "a58ee1d3039b8bec838e6d1482a8a965",
4
+ "Name": "List One"
5
+ },
6
+ {
7
+ "ListID": "99bc35084a5739127a8ab81eae5bd305",
8
+ "Name": "List Two"
9
+ }
10
+ ]
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "ListID": "a58ee1d3039b8bec838e6d1482a8a965",
4
+ "Name": "Segment One"
5
+ },
6
+ {
7
+ "ListID": "8dffb94c60c5faa3d40f496f2aa58a8a",
8
+ "Name": "Segment Two"
9
+ }
10
+ ]
@@ -0,0 +1,20 @@
1
+ {
2
+ "EmailAddress": "subscriber@example.com",
3
+ "Name": "Subscriber One",
4
+ "Date": "2010-10-25 10:28:00",
5
+ "State": "Active",
6
+ "CustomFields": [
7
+ {
8
+ "Key": "website",
9
+ "Value": "http://example.com"
10
+ },
11
+ {
12
+ "Key": "age",
13
+ "Value": "24"
14
+ },
15
+ {
16
+ "Key": "subscription date",
17
+ "Value": "2010-03-09"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,45 @@
1
+ [
2
+ {
3
+ "ID": "fc0ce7105baeaf97f47c99be31d02a91",
4
+ "Type": "Campaign",
5
+ "Name": "Campaign One",
6
+ "Actions": [
7
+ {
8
+ "Event": "Open",
9
+ "Date": "2010-10-12 13:18:00",
10
+ "IPAddress": "192.168.126.87",
11
+ "Detail": ""
12
+ },
13
+ {
14
+ "Event": "Click",
15
+ "Date": "2010-10-12 13:16:00",
16
+ "IPAddress": "192.168.126.87",
17
+ "Detail": "http://example.com/post/12323/"
18
+ },
19
+ {
20
+ "Event": "Click",
21
+ "Date": "2010-10-12 13:15:00",
22
+ "IPAddress": "192.168.126.87",
23
+ "Detail": "http://example.com/post/29889/"
24
+ },
25
+ {
26
+ "Event": "Open",
27
+ "Date": "2010-10-12 13:15:00",
28
+ "IPAddress": "192.168.126.87",
29
+ "Detail": ""
30
+ },
31
+ {
32
+ "Event": "Click",
33
+ "Date": "2010-10-12 13:01:00",
34
+ "IPAddress": "192.168.126.87",
35
+ "Detail": "http://example.com/post/82211/"
36
+ },
37
+ {
38
+ "Event": "Open",
39
+ "Date": "2010-10-12 13:01:00",
40
+ "IPAddress": "192.168.126.87",
41
+ "Detail": ""
42
+ }
43
+ ]
44
+ }
45
+ ]
@@ -0,0 +1,12 @@
1
+ [
2
+ {
3
+ "EmailAddress": "subs+098u0qu0qwd@example.com",
4
+ "Date": "2009-11-25 13:23:20",
5
+ "State": "Suppressed"
6
+ },
7
+ {
8
+ "EmailAddress": "subs+3018006@example.com",
9
+ "Date": "2009-11-10 11:50:04",
10
+ "State": "Suppressed"
11
+ }
12
+ ]
@@ -0,0 +1,3 @@
1
+ {
2
+ "SystemDate": "2010-10-15 09:27:00"
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "TemplateID": "98y2e98y289dh89h938389",
3
+ "Name": "Template One",
4
+ "PreviewURL": "http://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=01AF532CD8889B33&d=r&c=E816F55BFAD1A753",
5
+ "ScreenshotURL": "http://preview.createsend.com/ts/r/14/833/263/14833263.jpg?0318092600"
6
+ }
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "TemplateID": "5cac213cf061dd4e008de5a82b7a3621",
4
+ "Name": "Template One",
5
+ "PreviewURL": "http://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=01AF532CD8889B33&d=r&c=E816F55BFAD1A753",
6
+ "ScreenshotURL": "http://preview.createsend.com/ts/r/14/833/263/14833263.jpg?0318092541"
7
+ },
8
+ {
9
+ "TemplateID": "da645c271bc85fb6550acff937c2ab2e",
10
+ "Name": "Template Two",
11
+ "PreviewURL": "http://preview.createsend.com/createsend/templates/previewTemplate.aspx?ID=C8A180629495E798&d=r&c=E816F55BFAD1A753",
12
+ "ScreenshotURL": "http://preview.createsend.com/ts/r/18/7B3/552/187B3552.jpg?0705043527"
13
+ }
14
+ ]
@@ -0,0 +1,99 @@
1
+ [
2
+ "(GMT) Casablanca",
3
+ "(GMT) Coordinated Universal Time",
4
+ "(GMT) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London",
5
+ "(GMT) Monrovia, Reykjavik",
6
+ "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna",
7
+ "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague",
8
+ "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris",
9
+ "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb",
10
+ "(GMT+01:00) West Central Africa",
11
+ "(GMT+02:00) Amman",
12
+ "(GMT+02:00) Athens, Bucharest, Istanbul",
13
+ "(GMT+02:00) Beirut",
14
+ "(GMT+02:00) Cairo",
15
+ "(GMT+02:00) Damascus",
16
+ "(GMT+02:00) Harare, Pretoria",
17
+ "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius",
18
+ "(GMT+02:00) Jerusalem",
19
+ "(GMT+02:00) Minsk",
20
+ "(GMT+02:00) Windhoek",
21
+ "(GMT+03:00) Baghdad",
22
+ "(GMT+03:00) Kuwait, Riyadh",
23
+ "(GMT+03:00) Moscow, St. Petersburg, Volgograd",
24
+ "(GMT+03:00) Nairobi",
25
+ "(GMT+03:30) Tehran",
26
+ "(GMT+04:00) Abu Dhabi, Muscat",
27
+ "(GMT+04:00) Baku",
28
+ "(GMT+04:00) Port Louis",
29
+ "(GMT+04:00) Tbilisi",
30
+ "(GMT+04:00) Yerevan",
31
+ "(GMT+04:30) Kabul",
32
+ "(GMT+05:00) Ekaterinburg",
33
+ "(GMT+05:00) Islamabad, Karachi",
34
+ "(GMT+05:00) Tashkent",
35
+ "(GMT+05:30) Chennai, Kolkata, Mumbai, New Delhi",
36
+ "(GMT+05:30) Sri Jayawardenepura",
37
+ "(GMT+05:45) Kathmandu",
38
+ "(GMT+06:00) Astana",
39
+ "(GMT+06:00) Dhaka",
40
+ "(GMT+06:00) Novosibirsk",
41
+ "(GMT+06:30) Yangon (Rangoon)",
42
+ "(GMT+07:00) Bangkok, Hanoi, Jakarta",
43
+ "(GMT+07:00) Krasnoyarsk",
44
+ "(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi",
45
+ "(GMT+08:00) Irkutsk",
46
+ "(GMT+08:00) Kuala Lumpur, Singapore",
47
+ "(GMT+08:00) Perth",
48
+ "(GMT+08:00) Taipei",
49
+ "(GMT+08:00) Ulaanbaatar",
50
+ "(GMT+09:00) Osaka, Sapporo, Tokyo",
51
+ "(GMT+09:00) Seoul",
52
+ "(GMT+09:00) Yakutsk",
53
+ "(GMT+09:30) Adelaide",
54
+ "(GMT+09:30) Darwin",
55
+ "(GMT+10:00) Brisbane",
56
+ "(GMT+10:00) Canberra, Melbourne, Sydney",
57
+ "(GMT+10:00) Guam, Port Moresby",
58
+ "(GMT+10:00) Hobart",
59
+ "(GMT+10:00) Vladivostok",
60
+ "(GMT+11:00) Magadan, Solomon Is., New Caledonia",
61
+ "(GMT+12:00) Auckland, Wellington",
62
+ "(GMT+12:00) Coordinated Universal Time+12",
63
+ "(GMT+12:00) Fiji",
64
+ "(GMT+12:00) Petropavlovsk-Kamchatsky - Old",
65
+ "(GMT+13:00) Nuku'alofa",
66
+ "(GMT-01:00) Azores",
67
+ "(GMT-01:00) Cape Verde Is.",
68
+ "(GMT-02:00) Coordinated Universal Time-02",
69
+ "(GMT-02:00) Mid-Atlantic",
70
+ "(GMT-03:00) Brasilia",
71
+ "(GMT-03:00) Buenos Aires",
72
+ "(GMT-03:00) Cayenne, Fortaleza",
73
+ "(GMT-03:00) Greenland",
74
+ "(GMT-03:00) Montevideo",
75
+ "(GMT-03:30) Newfoundland",
76
+ "(GMT-04:00) Asuncion",
77
+ "(GMT-04:00) Atlantic Time (Canada)",
78
+ "(GMT-04:00) Cuiaba",
79
+ "(GMT-04:00) Georgetown, La Paz, Manaus, San Juan",
80
+ "(GMT-04:00) Santiago",
81
+ "(GMT-04:30) Caracas",
82
+ "(GMT-05:00) Bogota, Lima, Quito",
83
+ "(GMT-05:00) Eastern Time (US & Canada)",
84
+ "(GMT-05:00) Indiana (East)",
85
+ "(GMT-06:00) Central America",
86
+ "(GMT-06:00) Central Time (US & Canada)",
87
+ "(GMT-06:00) Guadalajara, Mexico City, Monterrey",
88
+ "(GMT-06:00) Saskatchewan",
89
+ "(GMT-07:00) Arizona",
90
+ "(GMT-07:00) Chihuahua, La Paz, Mazatlan",
91
+ "(GMT-07:00) Mountain Time (US & Canada)",
92
+ "(GMT-08:00) Baja California",
93
+ "(GMT-08:00) Pacific Time (US & Canada)",
94
+ "(GMT-09:00) Alaska",
95
+ "(GMT-10:00) Hawaii",
96
+ "(GMT-11:00) Coordinated Universal Time-11",
97
+ "(GMT-11:00) Samoa",
98
+ "(GMT-12:00) International Date Line West"
99
+ ]
@@ -0,0 +1,21 @@
1
+ [
2
+ {
3
+ "EmailAddress": "subscriber@example.com",
4
+ "Name": "Unsub One",
5
+ "Date": "2010-10-25 13:11:00",
6
+ "State": "Unsubscribed",
7
+ "CustomFields": []
8
+ },
9
+ {
10
+ "EmailAddress": "subscriberone@example.com",
11
+ "Name": "Unsub Two",
12
+ "Date": "2010-10-25 13:04:00",
13
+ "State": "Unsubscribed",
14
+ "CustomFields": [
15
+ {
16
+ "Key": "website",
17
+ "Value": "http://google.com"
18
+ }
19
+ ]
20
+ }
21
+ ]
data/test/helper.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require 'pathname'
3
+
4
+ require 'shoulda'
5
+ require 'matchy'
6
+ require 'mocha'
7
+ require 'fakeweb'
8
+
9
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
11
+ require 'createsend'
12
+
13
+ FakeWeb.allow_net_connect = false
14
+
15
+ def fixture_file(filename)
16
+ return '' if filename == ''
17
+ file_path = File.expand_path(File.dirname(__FILE__) + '/fixtures/' + filename)
18
+ File.read(file_path)
19
+ end
20
+
21
+ def createsend_url(api_key, url)
22
+ api_key.nil? ? url : url =~ /^http/ ? url : "http://#{api_key}:x@api.createsend.com/api/v3/#{url}"
23
+ end
24
+
25
+ def stub_request(method, api_key, url, filename, status=nil)
26
+ options = {:body => ""}
27
+ options.merge!({:body => fixture_file(filename)}) if filename
28
+ options.merge!({:status => status}) if status
29
+ options.merge!(:content_type => "application/json")
30
+ FakeWeb.register_uri(method, createsend_url(api_key, url), options)
31
+ end
32
+
33
+ def stub_get(*args); stub_request(:get, *args) end
34
+ def stub_post(*args); stub_request(:post, *args) end
35
+ def stub_put(*args); stub_request(:put, *args) end
36
+ def stub_delete(*args); stub_request(:delete, *args) end
data/test/list_test.rb ADDED
@@ -0,0 +1,107 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class ListTest < Test::Unit::TestCase
4
+ context "when an api caller is authenticated" do
5
+ setup do
6
+ @api_key = '123123123123123123123'
7
+ CreateSend.api_key @api_key
8
+ @client_id = "87y8d7qyw8d7yq8w7ydwqwd"
9
+ @list_id = "e3c5f034d68744f7881fdccf13c2daee"
10
+ @list = List.new @list_id
11
+ end
12
+
13
+ should "create a list" do
14
+ stub_post(@api_key, "lists/#{@client_id}.json", "create_list.json")
15
+ list_id = List.create @client_id, "List One", "", false, ""
16
+ list_id.should == "e3c5f034d68744f7881fdccf13c2daee"
17
+ end
18
+
19
+ should "update a list" do
20
+ stub_put(@api_key, "lists/#{@list.list_id}.json", nil)
21
+ @list.update "List One Renamed", "", false, ""
22
+ end
23
+
24
+ should "delete a list" do
25
+ stub_delete(@api_key, "lists/#{@list.list_id}.json", nil)
26
+ @list.delete
27
+ end
28
+
29
+ should "create a custom field" do
30
+ stub_post(@api_key, "lists/#{@list.list_id}/customfields.json", "create_custom_field.json")
31
+ personalisation_tag = @list.create_custom_field "new date field", "Date"
32
+ personalisation_tag.should == "[newdatefield]"
33
+ end
34
+
35
+ should "delete a custom field" do
36
+ custom_field_key = "[newdatefield]"
37
+ stub_delete(@api_key, "lists/#{@list.list_id}/customfields/#{CGI.escape(custom_field_key)}.json", nil)
38
+ @list.delete_custom_field custom_field_key
39
+ end
40
+
41
+ should "get the details of a list" do
42
+ stub_get(@api_key, "lists/#{@list.list_id}.json", "list_details.json")
43
+ details = @list.details
44
+ details.ConfirmedOptIn.should == false
45
+ details.Title.should == "a non-basic list :)"
46
+ details.UnsubscribePage.should == ""
47
+ details.ListID.should == "2fe4c8f0373ce320e2200596d7ef168f"
48
+ details.ConfirmationSuccessPage.should == ""
49
+ end
50
+
51
+ should "get the custom fields for a list" do
52
+ stub_get(@api_key, "lists/#{@list.list_id}/customfields.json", "custom_fields.json")
53
+ cfs = @list.custom_fields
54
+ cfs.size.should == 3
55
+ cfs.first.FieldName.should == "website"
56
+ cfs.first.Key.should == "[website]"
57
+ cfs.first.DataType.should == "Text"
58
+ cfs.first.FieldOptions.should == []
59
+ end
60
+
61
+ should "get the stats for a list" do
62
+ stub_get(@api_key, "lists/#{@list.list_id}/stats.json", "list_stats.json")
63
+ stats = @list.stats
64
+ stats.TotalActiveSubscribers.should == 6
65
+ stats.TotalUnsubscribes.should == 2
66
+ stats.TotalDeleted.should == 0
67
+ stats.TotalBounces.should == 0
68
+ end
69
+
70
+ should "get the active subscribers for a list" do
71
+ min_date = "2010-01-01"
72
+ stub_get(@api_key, "lists/#{@list.list_id}/active.json?date=#{CGI.escape(min_date)}", "active_subscribers.json")
73
+ active = @list.active min_date
74
+ active.size.should == 6
75
+ active.first.EmailAddress.should == "subs+7t8787Y@example.com"
76
+ active.first.Name.should == "Subscriber One"
77
+ active.first.Date.should == "2010-10-25 10:28:00"
78
+ active.first.State.should == "Active"
79
+ active.first.CustomFields.size.should == 3
80
+ end
81
+
82
+ should "get the unsubscribed subscribers for a list" do
83
+ min_date = "2010-01-01"
84
+ stub_get(@api_key, "lists/#{@list.list_id}/unsubscribed.json?date=#{CGI.escape(min_date)}", "unsubscribed_subscribers.json")
85
+ unsub = @list.unsubscribed min_date
86
+ unsub.size.should == 2
87
+ unsub.first.EmailAddress.should == "subscriber@example.com"
88
+ unsub.first.Name.should == "Unsub One"
89
+ unsub.first.Date.should == "2010-10-25 13:11:00"
90
+ unsub.first.State.should == "Unsubscribed"
91
+ unsub.first.CustomFields.size.should == 0
92
+ end
93
+
94
+ should "get the bounced subscribers for a list" do
95
+ min_date = "2010-01-01"
96
+ stub_get(@api_key, "lists/#{@list.list_id}/bounced.json?date=#{CGI.escape(min_date)}", "bounced_subscribers.json")
97
+ bounced = @list.bounced min_date
98
+ bounced.size.should == 1
99
+ bounced.first.EmailAddress.should == "bouncedsubscriber@example.com"
100
+ bounced.first.Name.should == "Bounced One"
101
+ bounced.first.Date.should == "2010-10-25 13:11:00"
102
+ bounced.first.State.should == "Bounced"
103
+ bounced.first.CustomFields.size.should == 0
104
+ end
105
+
106
+ end
107
+ end