infopark_webcrm_sdk 1.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|