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.
- checksums.yaml +7 -0
- data/LICENSE +840 -0
- data/README.md +113 -0
- data/UPGRADE.md +507 -0
- data/config/ca-bundle.crt +4484 -0
- data/lib/crm/account.rb +17 -0
- data/lib/crm/activity.rb +143 -0
- data/lib/crm/collection.rb +41 -0
- data/lib/crm/contact.rb +121 -0
- data/lib/crm/core/attachment_store.rb +122 -0
- data/lib/crm/core/basic_resource.rb +68 -0
- data/lib/crm/core/configuration.rb +65 -0
- data/lib/crm/core/connection_manager.rb +98 -0
- data/lib/crm/core/item_enumerator.rb +61 -0
- data/lib/crm/core/log_subscriber.rb +41 -0
- data/lib/crm/core/mixins/attribute_provider.rb +135 -0
- data/lib/crm/core/mixins/change_loggable.rb +98 -0
- data/lib/crm/core/mixins/findable.rb +28 -0
- data/lib/crm/core/mixins/inspectable.rb +27 -0
- data/lib/crm/core/mixins/merge_and_deletable.rb +17 -0
- data/lib/crm/core/mixins/modifiable.rb +102 -0
- data/lib/crm/core/mixins/searchable.rb +88 -0
- data/lib/crm/core/mixins.rb +6 -0
- data/lib/crm/core/rest_api.rb +148 -0
- data/lib/crm/core/search_configurator.rb +207 -0
- data/lib/crm/core.rb +6 -0
- data/lib/crm/errors.rb +169 -0
- data/lib/crm/event.rb +17 -0
- data/lib/crm/event_contact.rb +16 -0
- data/lib/crm/mailing.rb +111 -0
- data/lib/crm/template_set.rb +81 -0
- data/lib/crm/type.rb +78 -0
- data/lib/crm.rb +154 -0
- data/lib/infopark_webcrm_sdk.rb +1 -0
- metadata +149 -0
@@ -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
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
|
data/lib/crm/mailing.rb
ADDED
@@ -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
|