infopark_webcrm_sdk 1.0.0.rc3

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.
@@ -0,0 +1,148 @@
1
+ require 'multi_json'
2
+ require 'addressable/uri'
3
+
4
+ module Crm; module Core
5
+ class RestApi
6
+ METHOD_TO_NET_HTTP_CLASS = {
7
+ :get => Net::HTTP::Get,
8
+ :put => Net::HTTP::Put,
9
+ :post => Net::HTTP::Post,
10
+ :delete => Net::HTTP::Delete,
11
+ }.freeze
12
+
13
+ def self.instance=(instance)
14
+ @instance = instance
15
+ end
16
+
17
+ def self.instance
18
+ if @instance
19
+ @instance
20
+ else
21
+ raise "Please run Crm.configure first"
22
+ end
23
+ end
24
+
25
+ def initialize(uri, login, api_key)
26
+ @uri = uri
27
+ @login = login
28
+ @api_key = api_key
29
+ @connection_manager = ConnectionManager.new(uri)
30
+ end
31
+
32
+ def get(resource_path, payload = nil)
33
+ response_for_request(:get, resource_path, payload, {})
34
+ end
35
+
36
+ def put(resource_path, payload, headers = {})
37
+ response_for_request(:put, resource_path, payload, headers)
38
+ end
39
+
40
+ def post(resource_path, payload)
41
+ response_for_request(:post, resource_path, payload, {})
42
+ end
43
+
44
+ def delete(resource_path, payload = nil, headers = {})
45
+ response_for_request(:delete, resource_path, payload, headers)
46
+ end
47
+
48
+ def resolve_uri(url)
49
+ input_uri = Addressable::URI.parse(url)
50
+ input_uri.path = Addressable::URI.escape(input_uri.path)
51
+ @uri + input_uri.to_s
52
+ end
53
+
54
+ private
55
+
56
+ def response_for_request(method, resource_path, payload, headers)
57
+ path = resolve_uri(resource_path).path
58
+ request = method_to_net_http_class(method).new(path)
59
+ set_headers(request, headers)
60
+ request.body = MultiJson.encode(payload) if payload.present?
61
+
62
+ response = nil
63
+ retried = false
64
+ begin
65
+ ActiveSupport::Notifications.instrument("request.crm") do |msg|
66
+ msg[:method] = method
67
+ msg[:resource_path] = "#{resource_path}"
68
+ msg[:request_payload] = payload
69
+ end
70
+ response = ActiveSupport::Notifications.instrument("response.crm") do |msg|
71
+ # lower timeout back to DEFAULT_TIMEOUT once the backend has been fixed
72
+ msg[:response] = @connection_manager.request(request, 25)
73
+ end
74
+ rescue Errors::NetworkError => e
75
+ if method == :post || retried
76
+ raise e
77
+ else
78
+ retried = true
79
+ retry
80
+ end
81
+ end
82
+
83
+ handle_response(response)
84
+ end
85
+
86
+ def parse_payload(payload)
87
+ MultiJson.load(payload)
88
+ rescue MultiJson::DecodeError
89
+ raise Errors::ServerError.new("Server returned invalid json: #{payload}")
90
+ end
91
+
92
+ def handle_response(response)
93
+ body = parse_payload(response.body)
94
+ if response.code.start_with?('2')
95
+ body
96
+ else
97
+ message = body['message']
98
+
99
+ case body['id']
100
+ when 'unauthorized'
101
+ raise Errors::UnauthorizedAccess.new(message)
102
+ when 'authentication_failed'
103
+ raise Errors::AuthenticationFailed.new(message)
104
+ when 'forbidden'
105
+ raise Errors::ForbiddenAccess.new(message)
106
+ when 'not_found'
107
+ raise Errors::ResourceNotFound.new(message, body['missing_ids'])
108
+ when 'item_state_precondition_failed'
109
+ raise Errors::ItemStatePreconditionFailed.new(message, body['unmet_preconditions'])
110
+ when 'conflict'
111
+ raise Errors::ResourceConflict.new(message)
112
+ when 'invalid_keys'
113
+ raise Errors::InvalidKeys.new(message, body['validation_errors'])
114
+ when 'invalid_values'
115
+ raise Errors::InvalidValues.new(message, body['validation_errors'])
116
+ # leaving out 'params_parse_error', since the client should always send valid json.
117
+ when 'rate_limit'
118
+ raise Errors::RateLimitExceeded.new(message)
119
+ when 'internal_server_error'
120
+ raise Errors::ServerError.new(message)
121
+ when 'too_many_params'
122
+ raise Errors::TooManyParams.new(message)
123
+ else
124
+ if response.code == '404'
125
+ raise Errors::ResourceNotFound.new('Not Found.')
126
+ elsif response.code.start_with?('4')
127
+ raise Errors::ClientError.new("HTTP Code #{response.code}: #{body}")
128
+ else
129
+ raise Errors::ServerError.new("HTTP Code #{response.code}: #{body}")
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def set_headers(request, additional_headers)
136
+ request.basic_auth(@login, @api_key)
137
+ request['Content-Type'] = 'application/json'
138
+ request['Accept'] = 'application/json'
139
+ additional_headers.each do |key, value|
140
+ request[key] = value
141
+ end
142
+ end
143
+
144
+ def method_to_net_http_class(method)
145
+ METHOD_TO_NET_HTTP_CLASS.fetch(method)
146
+ end
147
+ end
148
+ end; end
@@ -0,0 +1,207 @@
1
+ module Crm; module Core
2
+ # +SearchConfigurator+ provides methods to incrementally configure a search request
3
+ # using chainable methods and to {#perform_search perform} this search.
4
+ # @example
5
+ # search_config = Crm::Contact.
6
+ # where('last_name', 'equals', 'Johnson').
7
+ # and('locality', 'equals', 'New York').
8
+ # and_not('language', 'equals', 'en').
9
+ # sort_by('first_name').
10
+ # sort_order('desc').
11
+ # offset(1).
12
+ # limit(3)
13
+ # # => Crm::Core::SearchConfigurator
14
+ #
15
+ # results = search_config.perform_search
16
+ # # => Crm::Core::ItemEnumerator
17
+ #
18
+ # results.length # => 3
19
+ # results.total # => 17
20
+ # results.map(&:first_name) # => ['Tim', 'Joe', 'Ann']
21
+ #
22
+ # @example Unlimited search results
23
+ # search_config = Crm::Contact.
24
+ # where('last_name', 'equals', 'Johnson').
25
+ # unlimited
26
+ # # => Crm::Core::SearchConfigurator
27
+ #
28
+ # results = search_config.perform_search
29
+ # # => Crm::Core::ItemEnumerator
30
+ #
31
+ # results.length # => 85
32
+ # results.total # => 85
33
+ # results.map(&:first_name) # => an array of 85 first names
34
+ # @api public
35
+ class SearchConfigurator
36
+ include Enumerable
37
+
38
+ def initialize(settings = {})
39
+ @settings = {
40
+ filters: [],
41
+ }.merge(settings)
42
+ end
43
+
44
+ # Executes the search request based on this configuration.
45
+ # @return [ItemEnumerator] the search result.
46
+ # @api public
47
+ def perform_search
48
+ @perform_search ||= Crm.search(
49
+ filters: @settings[:filters],
50
+ query: @settings[:query],
51
+ limit: @settings[:limit],
52
+ offset: @settings[:offset],
53
+ sort_by: @settings[:sort_by],
54
+ sort_order: @settings[:sort_order],
55
+ include_deleted: @settings[:include_deleted]
56
+ )
57
+ end
58
+
59
+ # @!group Chainable methods
60
+
61
+ # Returns a new {SearchConfigurator} constructed by combining
62
+ # this configuration and the new filter.
63
+ #
64
+ # Supported conditions:
65
+ # * +contains_word_prefixes+ - +field+ contains words starting with +value+.
66
+ # * +contains_words+ - +field+ contains the words given by +value+.
67
+ # * +equals+ - +field+ exactly corresponds to +value+ (case insensitive).
68
+ # * +is_blank+ - +field+ is blank (omit +value+).
69
+ # * +is_earlier_than+ - date time +field+ is earlier than +value+.
70
+ # * +is_later_than+ - date time +field+ is later than +value+.
71
+ # * +is_true+ - +field+ is true (omit +value+).
72
+ # @param field [Symbol, String] the attribute name.
73
+ # @param condition [Symbol, String] the condition, e.g. +:equals+.
74
+ # @param value [Symbol, String, Array<Symbol, String>, nil] the value.
75
+ # @return [SearchConfigurator]
76
+ # @api public
77
+ def add_filter(field, condition, value = nil)
78
+ new_filter = Array(@settings[:filters]) + [{field: field, condition: condition, value: value}]
79
+ SearchConfigurator.new(@settings.merge(filters: new_filter))
80
+ end
81
+ alias and add_filter
82
+
83
+ # Returns a new {SearchConfigurator} constructed by combining
84
+ # this configuration and the new negated filter.
85
+ #
86
+ # All filters (and their conditions) passed to {#add_filter} can be negated.
87
+ # @param field [Symbol, String] the attribute name.
88
+ # @param condition [Symbol, String] the condition, e.g. +:equals+.
89
+ # @param value [Symbol, String, Array<Symbol, String>, nil] the value.
90
+ # @return [SearchConfigurator]
91
+ # @api public
92
+ def add_negated_filter(field, condition, value = nil)
93
+ negated_condition = "not_#{condition}"
94
+ add_filter(field, negated_condition, value)
95
+ end
96
+ alias and_not add_negated_filter
97
+
98
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
99
+ # with the given query.
100
+ # @param new_query [String] the new query.
101
+ # @return [SearchConfigurator]
102
+ # @api public
103
+ def query(new_query)
104
+ SearchConfigurator.new(@settings.merge(query: new_query))
105
+ end
106
+
107
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
108
+ # with the given limit.
109
+ # @param new_limit [Fixnum] the new limit.
110
+ # @return [SearchConfigurator]
111
+ # @api public
112
+ def limit(new_limit)
113
+ SearchConfigurator.new(@settings.merge(limit: new_limit))
114
+ end
115
+
116
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
117
+ # without limiting the number of search results.
118
+ # @return [SearchConfigurator]
119
+ # @api public
120
+ def unlimited
121
+ limit(:none)
122
+ end
123
+
124
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
125
+ # with the given offset.
126
+ # @param new_offset [Fixnum] the new offset.
127
+ # @return [SearchConfigurator]
128
+ # @api public
129
+ def offset(new_offset)
130
+ SearchConfigurator.new(@settings.merge(offset: new_offset))
131
+ end
132
+
133
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
134
+ # with the given sort criterion.
135
+ # @param new_sort_by [String]
136
+ # See {Crm.search} for the list of supported +sort_by+ values.
137
+ # @return [SearchConfigurator]
138
+ # @api public
139
+ def sort_by(new_sort_by)
140
+ SearchConfigurator.new(@settings.merge(sort_by: new_sort_by))
141
+ end
142
+
143
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
144
+ # with the given sort order.
145
+ # @param new_sort_order [String]
146
+ # See {Crm.search} for the list of supported +sort_order+ values.
147
+ # @return [SearchConfigurator]
148
+ # @api public
149
+ def sort_order(new_sort_order)
150
+ SearchConfigurator.new(@settings.merge(sort_order: new_sort_order))
151
+ end
152
+
153
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
154
+ # with the ascending sort order.
155
+ # @return [SearchConfigurator]
156
+ # @api public
157
+ def asc
158
+ sort_order('asc')
159
+ end
160
+
161
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
162
+ # with the descending sort order.
163
+ # @return [SearchConfigurator]
164
+ # @api public
165
+ def desc
166
+ sort_order('desc')
167
+ end
168
+
169
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
170
+ # with the given +include_deleted+ flag.
171
+ # @param new_include_deleted [Boolean] whether to include deleted items in the results.
172
+ # @return [SearchConfigurator]
173
+ # @api public
174
+ def include_deleted(new_include_deleted = true)
175
+ SearchConfigurator.new(@settings.merge(include_deleted: new_include_deleted))
176
+ end
177
+
178
+ # Returns a new {SearchConfigurator} constructed by combining this configuration
179
+ # and excluding deleted items.
180
+ # @return [SearchConfigurator]
181
+ # @api public
182
+ def exclude_deleted
183
+ include_deleted(false)
184
+ end
185
+
186
+ # @!endgroup
187
+
188
+ # Iterates over the search results.
189
+ # Implicitly triggers {#perform_search} and caches its result.
190
+ # See {ItemEnumerator#each} for details.
191
+ # @api public
192
+ def each(&block)
193
+ return enum_for(:each) unless block_given?
194
+
195
+ perform_search.each(&block)
196
+ end
197
+
198
+ # Returns the total number of items that match this search configuration.
199
+ # It can be greater than +limit+.
200
+ # Implicitly triggers {#perform_search} and caches its result.
201
+ # @return [Fixnum] the total.
202
+ # @api public
203
+ def total
204
+ perform_search.total
205
+ end
206
+ end
207
+ end; end
data/lib/crm/core.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Crm
2
+ # @api public
3
+ module Core
4
+ Crm.autoload_module(self, File.expand_path(__FILE__))
5
+ end
6
+ end
data/lib/crm/errors.rb ADDED
@@ -0,0 +1,169 @@
1
+ module Crm
2
+ # @api public
3
+ module Errors
4
+ # +BaseError+ is the superclass of all Infopark WebCRM SDK errors.
5
+ # @api public
6
+ class BaseError < StandardError
7
+ end
8
+
9
+ # +ServerError+ is raised if an internal error occurs in the API server.
10
+ # @api public
11
+ class ServerError < BaseError
12
+ end
13
+
14
+ # +ClientError+ is the superclass of all errors that are a result of client-supplied input.
15
+ # @api public
16
+ class ClientError < BaseError
17
+ end
18
+
19
+ # +UnauthorizedAccess+ is raised if the API user credentials are invalid.
20
+ # Set the correct API user credentials using {Crm.configure}.
21
+ # @api public
22
+ class UnauthorizedAccess < ClientError
23
+ end
24
+
25
+ # +AuthenticationFailed+ is raised if the credentials used with
26
+ # {Crm::Contact.authenticate!} are invalid.
27
+ # @api public
28
+ class AuthenticationFailed < ClientError
29
+ end
30
+
31
+ # +ForbiddenAccess+ is raised if the API user is not permitted to access the resource.
32
+ # @api public
33
+ class ForbiddenAccess < ClientError
34
+ end
35
+
36
+ # +TooManyParams+ is raised if more than 1000 keys are passed as parameters to
37
+ # {Core::Mixins::Modifiable::ClassMethods#create Modifiable.create} or
38
+ # {Core::Mixins::Modifiable#update Modifiable#update}.
39
+ # @api public
40
+ class TooManyParams < ClientError
41
+ end
42
+
43
+ # +ResourceNotFound+ is raised if the requested IDs could not be found.
44
+ # @api public
45
+ class ResourceNotFound < ClientError
46
+ # Returns the IDs that could not be found.
47
+ # @return [Array<String>]
48
+ # @example
49
+ # ["9762b2b4382f6bf34adbdeb21ce588aa"]
50
+ # @api public
51
+ attr_reader :missing_ids
52
+
53
+ def initialize(message = nil, missing_ids = [])
54
+ super("#{message} Missing IDs: #{missing_ids.to_sentence}")
55
+
56
+ @missing_ids = missing_ids
57
+ end
58
+ end
59
+
60
+ # +ItemStatePreconditionFailed+ is raised if one or more preconditions
61
+ # for the attempted action were not satisfied. For example, a deleted item cannot be updated.
62
+ # It must be undeleted first.
63
+ # @api public
64
+ class ItemStatePreconditionFailed < ClientError
65
+ # Returns the unmet preconditions.
66
+ # The items in the list are hashes consisting of a +code+ (the name of the precondition),
67
+ # and an English translation (+message+).
68
+ # @return [Array<Hash{String => String}>]
69
+ # @example
70
+ # [
71
+ # {
72
+ # "code" => "is_internal_mailing",
73
+ # "message" => "The mailing is not an internal mailing.",
74
+ # },
75
+ # ]
76
+ # @api public
77
+ attr_reader :unmet_preconditions
78
+
79
+ def initialize(message = nil, unmet_preconditions)
80
+ precondition_messages = unmet_preconditions.map{ |p| p['message'] }
81
+ new_message = ([message] + precondition_messages).join(' ')
82
+ super(new_message)
83
+
84
+ @unmet_preconditions = unmet_preconditions
85
+ end
86
+ end
87
+
88
+ # +ResourceConflict+ is raised if the item has been changed concurrently.
89
+ # {Core::BasicResource#reload Reload} the item, review the changes and retry.
90
+ # @api public
91
+ class ResourceConflict < ClientError
92
+ end
93
+
94
+ # +InvalidKeys+ is raised if a create or update request contains unknown attributes.
95
+ # @api public
96
+ class InvalidKeys < ClientError
97
+ # Returns the list of validation errors.
98
+ # The items in the list are hashes consisting of a +code+ (always +unknown+),
99
+ # the invalid +attribute+ name and an English translation (+message+).
100
+ # @return [Array<Hash{String => String}>]
101
+ # @example
102
+ # [
103
+ # {
104
+ # "attribute" => "foo",
105
+ # "code" => "unknown",
106
+ # "message" => "foo is unknown",
107
+ # },
108
+ # ]
109
+ # @api public
110
+ attr_reader :validation_errors
111
+
112
+ def initialize(message = nil, validation_errors = {})
113
+ super("#{message} #{validation_errors.map{ |h| h['message'] }.to_sentence}.")
114
+
115
+ @validation_errors = validation_errors
116
+ end
117
+ end
118
+
119
+ # +InvalidValues+ is raised if the keys of a create or update request are recognized
120
+ # but include incorrect values.
121
+ # @api public
122
+ class InvalidValues < ClientError
123
+ # Returns the list of validation errors.
124
+ # The items in the list are hashes consisting of a +code+ (the name of the validation error,
125
+ # i.e. one of the rails validation error codes),
126
+ # an +attribute+ name and an English translation (+message+).
127
+ # You may use +code+ to translate the message into other languages.
128
+ # @example
129
+ # [
130
+ # {
131
+ # "code" => "blank",
132
+ # "attribute" => "name",
133
+ # "message" => "name is blank",
134
+ # },
135
+ # ]
136
+ # @return [Array<Hash{String => String}>]
137
+ # @api public
138
+ attr_reader :validation_errors
139
+
140
+ def initialize(message = nil, validation_errors = {})
141
+ super("#{message} #{validation_errors.map{ |h| h['message'] }.to_sentence}.")
142
+
143
+ @validation_errors = validation_errors
144
+ end
145
+ end
146
+
147
+ # +RateLimitExceeded+ is raised if too many requests were issued within a given time frame.
148
+ # @api public
149
+ class RateLimitExceeded < ClientError
150
+ end
151
+
152
+ # +NetworkError+ is raised if a non-recoverable network-related error occurred
153
+ # (e.g. connection timeout).
154
+ # @api public
155
+ class NetworkError < BaseError
156
+ # Returns the underlying network error.
157
+ # E.g. {http://www.ruby-doc.org/stdlib/libdoc/timeout/rdoc/Timeout/Error.html Timeout::Error}
158
+ # @return [Exception]
159
+ # @api public
160
+ attr_reader :cause
161
+
162
+ def initialize(message = nil, cause = nil)
163
+ super(message)
164
+
165
+ @cause = cause
166
+ end
167
+ end
168
+ end
169
+ end
data/lib/crm/event.rb ADDED
@@ -0,0 +1,17 @@
1
+ module Crm
2
+ # An Infopark WebCRM event contains all data associated with an event such
3
+ # as a conference or a trade show. An event has participants ({EventContact}).
4
+ # @api public
5
+ class Event < Core::BasicResource
6
+ include Core::Mixins::Findable
7
+ include Core::Mixins::Modifiable
8
+ include Core::Mixins::ChangeLoggable
9
+ include Core::Mixins::Searchable
10
+ include Core::Mixins::Inspectable
11
+ inspectable :id, :title
12
+
13
+ # @!parse extend Core::Mixins::Findable::ClassMethods
14
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
15
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ module Crm
2
+ # An Infopark WebCRM event contact is a participant or a potential participant of an {Event}.
3
+ # @api public
4
+ class EventContact < Core::BasicResource
5
+ include Core::Mixins::Findable
6
+ include Core::Mixins::Modifiable
7
+ include Core::Mixins::ChangeLoggable
8
+ include Core::Mixins::Searchable
9
+ include Core::Mixins::Inspectable
10
+ inspectable :id
11
+
12
+ # @!parse extend Core::Mixins::Findable::ClassMethods
13
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
14
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
15
+ end
16
+ end
@@ -0,0 +1,111 @@
1
+ module Crm
2
+ # The purpose of an Infopark WebCRM mailing is to send an e-mail, e.g. a newsletter,
3
+ # to several recipients.
4
+ # The e-mails will be sent to the members of the contact collection associated with the mailing
5
+ # (+mailing.collection_id+).
6
+ #
7
+ # Infopark WebCRM uses the {http://liquidmarkup.org/ Liquid template engine} for evaluating
8
+ # mailing content.
9
+ # @api public
10
+ class Mailing < Core::BasicResource
11
+ include Core::Mixins::Findable
12
+ include Core::Mixins::Modifiable
13
+ include Core::Mixins::ChangeLoggable
14
+ include Core::Mixins::Searchable
15
+ include Core::Mixins::Inspectable
16
+ inspectable :id, :title
17
+
18
+ # @!parse extend Core::Mixins::Findable::ClassMethods
19
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
20
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
21
+
22
+ # Renders a preview of the e-mail for the given contact.
23
+ # @example
24
+ # mailing.html_body
25
+ # # => "<h1>Welcome {{contact.first_name}} {{contact.last_name}}</h1>"
26
+ #
27
+ # contact.email
28
+ # # => "john.doe@example.com"
29
+ #
30
+ # mailing.render_preview(contact)
31
+ # # => {
32
+ # # "email_from" => "Marketing <marketing@example.org>",
33
+ # # "email_reply_to" => "marketing-replyto@example.com",
34
+ # # "email_subject" => "Invitation to exhibition",
35
+ # # "email_to" => "john.doe@example.com",
36
+ # # "text_body" => "Welcome John Doe",
37
+ # # "html_body" => "<h1>Welcome John Doe</h1>"
38
+ # # }
39
+ # @param render_for_contact_or_id [String, Contact]
40
+ # the contact for which the e-mail preview is rendered.
41
+ # @return [Hash{String => String}] the values of the mailing fields evaluated
42
+ # in the context of the contact.
43
+ # @api public
44
+ def render_preview(render_for_contact_or_id)
45
+ Core::RestApi.instance.post("#{path}/render_preview", {
46
+ 'render_for_contact_id' => extract_id(render_for_contact_or_id)
47
+ })
48
+ end
49
+
50
+ # Sends a proof e-mail (personalized for a contact) to the current user (the API user).
51
+ # @example
52
+ # mailing.send_me_a_proof_email(contact)
53
+ # # => {
54
+ # # "message" => "e-mail sent to api_user@example.com"
55
+ # # }
56
+ # @param render_for_contact_or_id [String, Contact]
57
+ # the contact for which the proof e-mail is rendered.
58
+ # @return [Hash{String => String}] a status report.
59
+ # @api public
60
+ def send_me_a_proof_email(render_for_contact_or_id)
61
+ Core::RestApi.instance.post("#{path}/send_me_a_proof_email", {
62
+ 'render_for_contact_id' => extract_id(render_for_contact_or_id)
63
+ })
64
+ end
65
+
66
+ # Sends this mailing to a single contact.
67
+ #
68
+ # Use case: If someone registers for a newsletter, you can send them the most recent issue
69
+ # that has already been released.
70
+ # @example
71
+ # contact.email
72
+ # # => "john.doe@example.org"
73
+ #
74
+ # mailing.released_at
75
+ # # => 2014-12-01 12:48:00 +0100
76
+ #
77
+ # mailing.send_single_email(contact)
78
+ # # => {
79
+ # # "message" => "e-mail sent to john.doe@example.org"
80
+ # # }
81
+ # @param recipient_contact_or_id [String, Contact]
82
+ # the contact to send a single e-mail to.
83
+ # @return [Hash{String => String}] a status report.
84
+ # @api public
85
+ def send_single_email(recipient_contact_or_id)
86
+ Core::RestApi.instance.post("#{path}/send_single_email", {
87
+ 'recipient_contact_id' => extract_id(recipient_contact_or_id)
88
+ })
89
+ end
90
+
91
+ # Releases this mailing.
92
+ #
93
+ # Sends the mailing to all recipients, marks the mailing as
94
+ # released (+released_at+, +released_by+), and also sets +planned_release_at+ to now.
95
+ # @return [self] the updated mailing.
96
+ # @api public
97
+ def release
98
+ load_attributes(Core::RestApi.instance.post("#{path}/release", {}))
99
+ end
100
+
101
+ private
102
+
103
+ def extract_id(contact_or_id)
104
+ if contact_or_id.respond_to?(:id)
105
+ contact_or_id.id
106
+ else
107
+ contact_or_id
108
+ end
109
+ end
110
+ end
111
+ end