docusign_rest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +193 -0
- data/Rakefile +10 -0
- data/docusign_rest.gemspec +26 -0
- data/example.rb +40 -0
- data/lib/docusign_rest.rb +14 -0
- data/lib/docusign_rest/client.rb +609 -0
- data/lib/docusign_rest/configuration.rb +49 -0
- data/lib/docusign_rest/railtie.rb +11 -0
- data/lib/docusign_rest/version.rb +3 -0
- data/lib/multipart_post/parts.rb +13 -0
- data/lib/tasks/docusign_task.rake +76 -0
- data/lib/tasks/docusign_task.rb +5 -0
- data/test.pdf +0 -0
- data/test/docusign_rest/client_test.rb +191 -0
- data/test/docusign_rest/configuration_test.rb +24 -0
- data/test/docusign_rest/docusign_rest_test.rb +7 -0
- data/test/helper.rb +12 -0
- data/test2.pdf +0 -0
- metadata +200 -0
data/.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
.rvmrc
|
19
|
+
test.rb
|
20
|
+
test/fixtures/vcr/*
|
21
|
+
test/docusign_login_config.rb
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Jon Kinney
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
# DocusignRest
|
2
|
+
|
3
|
+
This 'wrapper gem' hooks a Ruby app (currently only tested with Rails) up to the DocuSign REST API to allow for embedded signing.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'docusign_rest'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install docusign_rest
|
18
|
+
|
19
|
+
## Configuration
|
20
|
+
|
21
|
+
There is a bundled rake task that will prompt you for your DocuSign credentials including:
|
22
|
+
|
23
|
+
* Username
|
24
|
+
* Password
|
25
|
+
* Integrator Key
|
26
|
+
|
27
|
+
and create the `config/initializers/docusign\_rest.rb` file in your Rails app for you. If the file was unable to be created, the rake task will output the config block for you to manually add to an initializer.
|
28
|
+
|
29
|
+
**Note** please run the below task and ensure your initializer is in place before attempting to use any of the methods in this gem. Without the initializer this gem will not be able to properly authenticate you to the DocuSign REST API.
|
30
|
+
|
31
|
+
$ bundle exec rake docusign_rest:generate_config
|
32
|
+
|
33
|
+
outputs:
|
34
|
+
|
35
|
+
Please do the following:
|
36
|
+
------------------------
|
37
|
+
1) Login or register for an account at demo.docusign.net
|
38
|
+
...or their production url if applicable
|
39
|
+
2) Click 'Preferences' in the upper right corner of the page
|
40
|
+
3) Click 'API' in far lower left corner of the menu
|
41
|
+
4) Request a new 'Integrator Key' via the web interface
|
42
|
+
* You will use this key in one of the next steps to retrieve your 'accountId'
|
43
|
+
|
44
|
+
Please enter your DocuSign username: someone@gmail.com
|
45
|
+
Please enter your DocuSign password: p@ssw0rd1
|
46
|
+
Please enter your DocuSign integrator_key: KEYS-19ddd1cc-cb56-4ca6-87ec-38db47d14b32
|
47
|
+
|
48
|
+
The following block of code was added to config/initializers/docusign_rest.rb
|
49
|
+
|
50
|
+
require 'docusign_rest'
|
51
|
+
|
52
|
+
DocusignRest.configure do |config|
|
53
|
+
config.username = 'someone@gmail.com'
|
54
|
+
config.password = 'p@ssw0rd1'
|
55
|
+
config.integrator_key = 'KEYS-19ddd1cc-cb56-4ca6-87ec-38db47d14b32'
|
56
|
+
config.account_id = '123456'
|
57
|
+
end
|
58
|
+
|
59
|
+
## Usage
|
60
|
+
|
61
|
+
The docusign\_rest gem makes creating multipart POST (aka file upload) requests to the DocuSign REST API dead simple. It's built on top of Net:HTTP and utilizes the [multipart-post](https://github.com/nicksieger/multipart-post) gem to assist with formatting the multipart requests for the DocuSign REST API. The DocuSign REST API requires that all files be embedded as JSON directly in the request body (not the body\_stream like multipart-post does by default) so the docusign\_rest gem takes care of setting that up for you.
|
62
|
+
|
63
|
+
This gem also monkey patches one small part of multipart-post to inject some header values and formatting that DocuSign requires. If you would like to see the monkey patched code please take a look at [lib/multipart-post/parts.rb](https://github.com/j2fly/docusign_rest/blob/master/lib/multipart_post/parts.rb). It's only re-opening one method, but feel free to make sure you understand that monkey patch if it concerns you.
|
64
|
+
|
65
|
+
### Examples
|
66
|
+
|
67
|
+
* These examples assume you have already run the `docusign_rest:generate_config` rake task and have the configure block properly setup in an initializer with your username, password, integrator\_key, and account\_id.
|
68
|
+
|
69
|
+
**Getting login information:**
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
client = DocusignRest::Client.new
|
73
|
+
puts client.get_account_id
|
74
|
+
```
|
75
|
+
|
76
|
+
|
77
|
+
**Creating an envelope from a document:**
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
client = DocusignRest::Client.new
|
81
|
+
response = client.create_envelope_from_document(
|
82
|
+
email: {
|
83
|
+
subject: "test email subject",
|
84
|
+
body: "this is the email body and it's large!"
|
85
|
+
},
|
86
|
+
# If embedded is set to true in the signers array below, emails
|
87
|
+
# don't go out and you can embed the signature page in an iFrame
|
88
|
+
# by using the get_recipient_view method
|
89
|
+
signers: [
|
90
|
+
{
|
91
|
+
#embedded: true,
|
92
|
+
name: 'Test Guy',
|
93
|
+
email: 'someone@gmail.com'
|
94
|
+
},
|
95
|
+
{
|
96
|
+
#embedded: true,
|
97
|
+
name: 'Test Girl',
|
98
|
+
email: 'someone@gmail.com'
|
99
|
+
}
|
100
|
+
],
|
101
|
+
files: [
|
102
|
+
{path: 'test.pdf', name: 'test.pdf'},
|
103
|
+
{path: 'test2.pdf', name: 'test2.pdf'}
|
104
|
+
],
|
105
|
+
status: 'sent'
|
106
|
+
)
|
107
|
+
response = JSON.parse(response.body)
|
108
|
+
response["status"].must_equal "sent"
|
109
|
+
```
|
110
|
+
|
111
|
+
|
112
|
+
**Creating a template:**
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
client = DocusignRest::Client.new
|
116
|
+
response = client.create_template(
|
117
|
+
description: 'Cool Description',
|
118
|
+
name: "Cool Template Name",
|
119
|
+
signers: [
|
120
|
+
{
|
121
|
+
embedded: true,
|
122
|
+
name: 'jon',
|
123
|
+
email: 'someone@gmail.com',
|
124
|
+
role_name: 'Issuer',
|
125
|
+
anchor_string: 'sign here'
|
126
|
+
}
|
127
|
+
],
|
128
|
+
files: [
|
129
|
+
{path: 'test.pdf', name: 'test.pdf'}
|
130
|
+
]
|
131
|
+
)
|
132
|
+
@template_response = JSON.parse(response.body)
|
133
|
+
```
|
134
|
+
|
135
|
+
|
136
|
+
**Creating an envelope from a template:**
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
client = DocusignRest::Client.new
|
140
|
+
response = client.create_envelope_from_template(
|
141
|
+
status: 'sent',
|
142
|
+
email: {
|
143
|
+
subject: "The test email subject envelope",
|
144
|
+
body: "Envelope body content here"
|
145
|
+
},
|
146
|
+
template_id: @template_response["templateId"],
|
147
|
+
signers: [
|
148
|
+
{
|
149
|
+
embedded: true,
|
150
|
+
name: 'jon',
|
151
|
+
email: 'someone@gmail.com',
|
152
|
+
role_name: 'Issuer'
|
153
|
+
}
|
154
|
+
]
|
155
|
+
)
|
156
|
+
@envelope_response = JSON.parse(response.body)
|
157
|
+
```
|
158
|
+
|
159
|
+
|
160
|
+
**Retrieving the url for embedded signing**
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
client = DocusignRest::Client.new
|
164
|
+
response = client.get_recipient_view(
|
165
|
+
envelope_id: @envelope_response["envelopeId"],
|
166
|
+
name: 'jon',
|
167
|
+
email: 'someone@gmail.com',
|
168
|
+
return_url: 'http://google.com'
|
169
|
+
)
|
170
|
+
@view_recipient_response = JSON.parse(response.body)
|
171
|
+
puts @view_recipient_response["url"]
|
172
|
+
```
|
173
|
+
|
174
|
+
|
175
|
+
## Contributing
|
176
|
+
|
177
|
+
1. Fork it
|
178
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
179
|
+
3. Commit your changes (`git commit -am 'Added some feature'`) making sure to write tests to ensure nothing breaks
|
180
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
181
|
+
5. Create new Pull Request
|
182
|
+
|
183
|
+
### Running the tests
|
184
|
+
|
185
|
+
In order to run the tests you'll need to register for a (free) DocuSign developer account. After doing so you'll have a username, password, and integrator key. Armed with that information execute the following ruby file:
|
186
|
+
|
187
|
+
$ ruby lib/tasks/docusign_task.rb
|
188
|
+
|
189
|
+
This calls a rake task which adds a non-version controlled file in the test folder called 'docusign_login_config.rb' which holds your account specific credentials and is required in order to hit the test API through the test suite.
|
190
|
+
|
191
|
+
**VCR**
|
192
|
+
|
193
|
+
The test suite uses VCR and is configured to record only the first request by using the 'once' configuration option surrounding each API request. If you want to experiment with the API or are getting several errors with the test suite, you may want to change the VCR config record setting to 'all' temporarily which will write a new YAML file for each request each time you hit the API. However, this significantly slow down tests and essentially negates the benefit of VCR which is to mock out the API entirely and keep the tests speedy.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/docusign_rest/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jon Kinney"]
|
6
|
+
gem.email = ["jonkinney@gmail.com"]
|
7
|
+
gem.description = %q{Hooks a Rails app up to the DocuSign service through the DocuSign REST API}
|
8
|
+
gem.summary = %q{Use this gem to embed signing of documents in a Rails app through the DocuSign REST API}
|
9
|
+
gem.homepage = "https://github.com/j2fly/docusign_rest"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "docusign_rest"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = DocusignRest::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency('multipart-post', '>= 1.1.5')
|
19
|
+
gem.add_dependency('json')
|
20
|
+
gem.add_development_dependency('rake')
|
21
|
+
gem.add_development_dependency('minitest')
|
22
|
+
gem.add_development_dependency('turn')
|
23
|
+
gem.add_development_dependency('pry')
|
24
|
+
gem.add_development_dependency('vcr')
|
25
|
+
gem.add_development_dependency('fakeweb')
|
26
|
+
end
|
data/example.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'docusign_rest'
|
2
|
+
|
3
|
+
DocusignRest.configure do |config|
|
4
|
+
config.username = "someone@gmail.com"
|
5
|
+
config.password = "password"
|
6
|
+
config.integrator_key = "KEYS-16dbc1bc-ca56-4ea6-87ec-29db47d94b32"
|
7
|
+
config.account_id = "123456"
|
8
|
+
end
|
9
|
+
|
10
|
+
client = DocusignRest::Client.new
|
11
|
+
|
12
|
+
response = client.create_envelope_from_document(
|
13
|
+
email: {
|
14
|
+
subject: "test email subject",
|
15
|
+
body: "this is the email body and it's large!"
|
16
|
+
},
|
17
|
+
# If embedded is set to true in the signers array below, emails
|
18
|
+
# don't go out and you can embed the signature page in an iFrame
|
19
|
+
# by using the get_recipient_view method
|
20
|
+
signers: [
|
21
|
+
{
|
22
|
+
#embedded: true,
|
23
|
+
name: 'Test Guy',
|
24
|
+
email: 'someone@gmail.com'
|
25
|
+
},
|
26
|
+
{
|
27
|
+
#embedded: true,
|
28
|
+
name: 'Test Girl',
|
29
|
+
email: 'someone+else@gmail.com'
|
30
|
+
}
|
31
|
+
],
|
32
|
+
files: [
|
33
|
+
{path: 'test.pdf', name: 'test.pdf'},
|
34
|
+
{path: 'test2.pdf', name: 'test2.pdf'}
|
35
|
+
],
|
36
|
+
status: 'sent'
|
37
|
+
)
|
38
|
+
|
39
|
+
response = JSON.parse(response.body)
|
40
|
+
puts response.body
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'docusign_rest/version'
|
2
|
+
require 'docusign_rest/configuration'
|
3
|
+
require 'docusign_rest/client'
|
4
|
+
require 'multipart_post' #require the multipart-post gem itself
|
5
|
+
require 'net/http/post/multipart' #require the multipart-post net/http/post/multipart monkey patch
|
6
|
+
require 'multipart_post/parts' #require my monkey patched parts.rb which adjusts the build_part method
|
7
|
+
require 'net/http'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
module DocusignRest
|
11
|
+
require "docusign_rest/railtie" if defined?(Rails)
|
12
|
+
|
13
|
+
extend Configuration
|
14
|
+
end
|
@@ -0,0 +1,609 @@
|
|
1
|
+
module DocusignRest
|
2
|
+
|
3
|
+
class Client
|
4
|
+
# Define the same set of accessors as the DocusignRest module
|
5
|
+
attr_accessor *Configuration::VALID_CONFIG_KEYS
|
6
|
+
attr_accessor :docusign_authentication_headers, :acct_id
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
# Merge the config values from the module and those passed to the client.
|
10
|
+
merged_options = DocusignRest.options.merge(options)
|
11
|
+
|
12
|
+
# Copy the merged values to this client and ignore those not part
|
13
|
+
# of our configuration
|
14
|
+
Configuration::VALID_CONFIG_KEYS.each do |key|
|
15
|
+
send("#{key}=", merged_options[key])
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set up the DocuSign Authentication headers with the values passed from
|
19
|
+
# our config block
|
20
|
+
@docusign_authentication_headers = {
|
21
|
+
"X-DocuSign-Authentication" => "" \
|
22
|
+
"<DocuSignCredentials>" \
|
23
|
+
"<Username>#{DocusignRest.username}</Username>" \
|
24
|
+
"<Password>#{DocusignRest.password}</Password>" \
|
25
|
+
"<IntegratorKey>#{DocusignRest.integrator_key}</IntegratorKey>" \
|
26
|
+
"</DocuSignCredentials>"
|
27
|
+
}
|
28
|
+
|
29
|
+
# Set the account_id from the configure block if present, but can't call
|
30
|
+
# the instance var @account_id because that'll override the attr_accessor
|
31
|
+
# that is automatically configured for the configure block
|
32
|
+
@acct_id = DocusignRest.account_id
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Internal: sets the default request headers allowing for user overrides
|
37
|
+
# via options[:headers] from within other requests. Additionally injects
|
38
|
+
# the X-DocuSign-Authentication header to authorize the request.
|
39
|
+
#
|
40
|
+
# Client can pass in header options to any given request:
|
41
|
+
# headers: {"Some-Key" => "some/value", "Another-Key" => "another/value"}
|
42
|
+
#
|
43
|
+
# Then we pass them on to this method to merge them with the other
|
44
|
+
# required headers
|
45
|
+
#
|
46
|
+
# Example:
|
47
|
+
#
|
48
|
+
# headers(options[:headers])
|
49
|
+
#
|
50
|
+
# Returns a merged hash of headers overriding the default Accept header if
|
51
|
+
# the user passes in a new "Accept" header key and adds any other
|
52
|
+
# user-defined headers along with the X-DocuSign-Authentication headers
|
53
|
+
def headers(user_defined_headers={})
|
54
|
+
default = {
|
55
|
+
"Accept" => "application/json" #this seems to get added automatically, so I can probably remove this
|
56
|
+
}
|
57
|
+
|
58
|
+
default.merge!(user_defined_headers) if user_defined_headers
|
59
|
+
|
60
|
+
@docusign_authentication_headers.merge(default)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Internal: builds a URI based on the configurable endpoint, api_version,
|
65
|
+
# and the passed in relative url
|
66
|
+
#
|
67
|
+
# url - a relative url requiring a leading forward slash
|
68
|
+
#
|
69
|
+
# Example:
|
70
|
+
#
|
71
|
+
# build_uri("/login_information")
|
72
|
+
#
|
73
|
+
# Returns a parsed URI object
|
74
|
+
def build_uri(url)
|
75
|
+
URI.parse("#{DocusignRest.endpoint}/#{DocusignRest.api_version}#{url}")
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Internal: configures Net:HTTP with some default values that are required
|
80
|
+
# for every request to the DocuSign API
|
81
|
+
#
|
82
|
+
# Returns a configured Net::HTTP object into which a request can be passed
|
83
|
+
def initialize_net_http_ssl(uri)
|
84
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
85
|
+
http.use_ssl = true
|
86
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
87
|
+
http
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
# Public: gets info necessary to make additional requests to the DocuSign API
|
92
|
+
#
|
93
|
+
# options - hash of headers if the client wants to override something
|
94
|
+
#
|
95
|
+
# Examples:
|
96
|
+
#
|
97
|
+
# client = DocusignRest::Client.new
|
98
|
+
# response = client.login_information
|
99
|
+
# puts response.body
|
100
|
+
#
|
101
|
+
# Returns:
|
102
|
+
# accountId - For the username, password, and integrator_key specified
|
103
|
+
# baseUrl - The base URL for all future DocuSign requests
|
104
|
+
# email - The email used when signing up for DocuSign
|
105
|
+
# isDefault - # TODO identify what this is
|
106
|
+
# name - The account name provided when signing up for DocuSign
|
107
|
+
# userId - # TODO determine what this is used for, if anything
|
108
|
+
# userName - Full name provided when signing up for DocuSign
|
109
|
+
def get_login_information(options={})
|
110
|
+
uri = build_uri("/login_information")
|
111
|
+
request = Net::HTTP::Get.new(uri.request_uri, headers(options[:headers]))
|
112
|
+
http = initialize_net_http_ssl(uri)
|
113
|
+
http.request(request)
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
# Internal: uses the get_login_information method to determine the client's
|
118
|
+
# accountId and then caches that value into an instance variable so we
|
119
|
+
# don't end up hitting the API for login_information more than once per
|
120
|
+
# request.
|
121
|
+
#
|
122
|
+
# This is used by the rake task in lib/tasks/docusign_task.rake to add
|
123
|
+
# the config/initialzers/docusign_rest.rb file with the proper config block
|
124
|
+
# which includes the account_id in it. That way we don't require hitting
|
125
|
+
# the /login_information URI in normal requests
|
126
|
+
#
|
127
|
+
# Returns the accountId string
|
128
|
+
def get_account_id
|
129
|
+
unless @acct_id
|
130
|
+
response = get_login_information.body
|
131
|
+
hashed_response = JSON.parse(response)
|
132
|
+
login_accounts = hashed_response['loginAccounts']
|
133
|
+
@acct_id ||= login_accounts.first['accountId']
|
134
|
+
end
|
135
|
+
|
136
|
+
@acct_id
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_embedded_signer(embedded, email)
|
140
|
+
if embedded && embedded == true
|
141
|
+
"\"clientUserId\" : \"#{email}\","
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Internal: takes in an array of hashes of signers and calculates the
|
146
|
+
# recipientId then concatenates all the hashes with commas
|
147
|
+
#
|
148
|
+
# embedded - Tells DocuSign if this is an embedded signer which determines
|
149
|
+
# weather or not to deliver emails. Also lets us authenticate
|
150
|
+
# them when they go to do embedded signing. Behind the scenes
|
151
|
+
# this is setting the clientUserId value to the signer's email.
|
152
|
+
# name - The name of the signer
|
153
|
+
# email - The email of the signer
|
154
|
+
#
|
155
|
+
# Returns a hash of users that need to sign the document
|
156
|
+
def get_signers(signers)
|
157
|
+
doc_signers = []
|
158
|
+
signers.each_with_index do |signer, index|
|
159
|
+
doc_signers << "{
|
160
|
+
#{check_embedded_signer(signer[:embedded], signer[:email])}
|
161
|
+
\"name\" : \"#{signer[:name]}\",
|
162
|
+
\"email\" : \"#{signer[:email]}\",
|
163
|
+
\"recipientId\" : \"#{index+1}\"
|
164
|
+
}"
|
165
|
+
end
|
166
|
+
doc_signers.join(",")
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
# Internal: takes in an array of hashes of signers and concatenates all the
|
171
|
+
# hashes with commas
|
172
|
+
#
|
173
|
+
# embedded - Tells DocuSign if this is an embedded signer which determines
|
174
|
+
# weather or not to deliver emails. Also lets us authenticate
|
175
|
+
# them when they go to do embedded signing. Behind the scenes
|
176
|
+
# this is setting the clientUserId value to the signer's email.
|
177
|
+
# name - The name of the signer
|
178
|
+
# email - The email of the signer
|
179
|
+
# role_name - The role name of the signer ('Attorney', 'Client', etc.).
|
180
|
+
#
|
181
|
+
# Returns a hash of users that need to be embedded in the template to
|
182
|
+
# create an envelope
|
183
|
+
def get_template_roles(signers)
|
184
|
+
template_roles = []
|
185
|
+
signers.each_with_index do |signer, index|
|
186
|
+
template_roles << "{
|
187
|
+
#{check_embedded_signer(signer[:embedded], signer[:email])}
|
188
|
+
\"name\" : \"#{signer[:name]}\",
|
189
|
+
\"email\" : \"#{signer[:email]}\",
|
190
|
+
\"roleName\" : \"#{signer[:role_name]}\"
|
191
|
+
}"
|
192
|
+
end
|
193
|
+
template_roles.join(",")
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
# Internal: takes an array of hashes of signers required to complete a
|
198
|
+
# document and allows for setting several options. Not all options are
|
199
|
+
# currently dynamic but that's easy to change/add which I (and I'm
|
200
|
+
# sure others) will be doing in the future.
|
201
|
+
#
|
202
|
+
# embedded - Tells DocuSign if this is an embedded signer which
|
203
|
+
# determines weather or not to deliver emails. Also
|
204
|
+
# lets us authenticate them when they go to do
|
205
|
+
# embedded signing. Behind the scenes this is setting
|
206
|
+
# the clientUserId value to the signer's email.
|
207
|
+
# email_notification - Send an email or not
|
208
|
+
# role_name - The signer's role, like 'Attorney' or 'Client', etc.
|
209
|
+
# template_locked - Doesn't seem to work/do anything
|
210
|
+
# template_required - Doesn't seem to work/do anything
|
211
|
+
# email - The signer's email
|
212
|
+
# name - The signer's name
|
213
|
+
# anchor_string - The string of text to anchor the 'sign here' tab to
|
214
|
+
# document_id - If the doc you want signed isn't the first doc in
|
215
|
+
# the files options hash
|
216
|
+
# page_number - Page number of the sign here tab
|
217
|
+
# x_position - Distance horizontally from the anchor string for the
|
218
|
+
# 'sign here' tab to appear. Note: doesn't seem to
|
219
|
+
# currently work.
|
220
|
+
# y_position - Distance vertically from the anchor string for the
|
221
|
+
# 'sign here' tab to appear. Note: doesn't seem to
|
222
|
+
# currently work.
|
223
|
+
# sign_here_tab_text - Instead of 'sign here'. Note: doesn't work
|
224
|
+
# tab_label - TODO: figure out what this is
|
225
|
+
def get_template_signers(signers)
|
226
|
+
doc_signers = []
|
227
|
+
signers.each_with_index do |signer, index|
|
228
|
+
doc_signers << "{
|
229
|
+
\"accessCode\":\"\",
|
230
|
+
\"addAccessCodeToEmail\":false,
|
231
|
+
#{check_embedded_signer(signer[:embedded], signer[:email])}
|
232
|
+
\"customFields\":null,
|
233
|
+
\"emailNotification\":#{signer[:email_notification] || 'null'},
|
234
|
+
\"idCheckConfigurationName\":null,
|
235
|
+
\"idCheckInformationInput\":null,
|
236
|
+
\"inheritEmailNotificationConfiguration\":false,
|
237
|
+
\"note\":\"\",
|
238
|
+
\"phoneAuthentication\":null,
|
239
|
+
\"recipientAttachments\":null,
|
240
|
+
\"recipientId\":\"#{index+1}\",
|
241
|
+
\"requireIdLookup\":false,
|
242
|
+
\"roleName\":\"#{signer[:role_name]}\",
|
243
|
+
\"routingOrder\":#{index+1},
|
244
|
+
\"socialAuthentications\":null,
|
245
|
+
\"templateAccessCodeRequired\":false,
|
246
|
+
\"templateLocked\":#{signer[:template_locked] || 'false'},
|
247
|
+
\"templateRequired\":#{signer[:template_required] || 'false'},
|
248
|
+
\"email\":\"#{signer[:email]}\",
|
249
|
+
\"name\":\"#{signer[:name]}\",
|
250
|
+
\"autoNavigation\":false,
|
251
|
+
\"defaultRecipient\":false,
|
252
|
+
\"signatureInfo\":null,
|
253
|
+
\"tabs\":{
|
254
|
+
\"approveTabs\":null,
|
255
|
+
\"checkboxTabs\":null,
|
256
|
+
\"companyTabs\":null,
|
257
|
+
\"dateSignedTabs\":null,
|
258
|
+
\"dateTabs\":null,
|
259
|
+
\"declineTabs\":null,
|
260
|
+
\"emailTabs\":null,
|
261
|
+
\"envelopeIdTabs\":null,
|
262
|
+
\"fullNameTabs\":null,
|
263
|
+
\"initialHereTabs\":null,
|
264
|
+
\"listTabs\":null,
|
265
|
+
\"noteTabs\":null,
|
266
|
+
\"numberTabs\":null,
|
267
|
+
\"radioGroupTabs\":null,
|
268
|
+
\"signHereTabs\":[
|
269
|
+
{
|
270
|
+
\"anchorString\":\"#{signer[:anchor_string]}\",
|
271
|
+
\"conditionalParentLabel\":null,
|
272
|
+
\"conditionalParentValue\":null,
|
273
|
+
\"documentId\":\"#{signer[:document_id] || '1'}\",
|
274
|
+
\"pageNumber\":\"#{signer[:page_number] || '1'}\",
|
275
|
+
\"recipientId\":\"#{index+1}\",
|
276
|
+
\"templateLocked\":#{signer[:template_locked] || 'false'},
|
277
|
+
\"templateRequired\":#{signer[:template_required] || 'false'},
|
278
|
+
\"xPosition\":\"#{signer[:x_position] || '0'}\",
|
279
|
+
\"yPosition\":\"#{signer[:y_position] || '0'}\",
|
280
|
+
\"name\":\"#{signer[:sign_here_tab_text] || 'Sign Here'}\",
|
281
|
+
\"optional\":false,
|
282
|
+
\"scaleValue\":1,
|
283
|
+
\"tabLabel\":\"#{signer[:tab_label] || 'Signature 1'}\"
|
284
|
+
}
|
285
|
+
],
|
286
|
+
\"signerAttachmentTabs\":null,
|
287
|
+
\"ssnTabs\":null,
|
288
|
+
\"textTabs\":null,
|
289
|
+
\"titleTabs\":null,
|
290
|
+
\"zipTabs\":null
|
291
|
+
}
|
292
|
+
}"
|
293
|
+
end
|
294
|
+
doc_signers.join(",")
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
# Internal: sets up the file ios array
|
299
|
+
#
|
300
|
+
# files - a hash of file params
|
301
|
+
#
|
302
|
+
# Returns the properly formatted ios used to build the file_params hash
|
303
|
+
def create_file_ios(files)
|
304
|
+
# UploadIO is from the multipart-post gem's lib/composite_io.rb:57
|
305
|
+
# where it has this documentation:
|
306
|
+
#
|
307
|
+
# ********************************************************************
|
308
|
+
# Create an upload IO suitable for including in the params hash of a
|
309
|
+
# Net::HTTP::Post::Multipart.
|
310
|
+
#
|
311
|
+
# Can take two forms. The first accepts a filename and content type, and
|
312
|
+
# opens the file for reading (to be closed by finalizer).
|
313
|
+
#
|
314
|
+
# The second accepts an already-open IO, but also requires a third argument,
|
315
|
+
# the filename from which it was opened (particularly useful/recommended if
|
316
|
+
# uploading directly from a form in a framework, which often save the file to
|
317
|
+
# an arbitrarily named RackMultipart file in /tmp).
|
318
|
+
#
|
319
|
+
# Usage:
|
320
|
+
#
|
321
|
+
# UploadIO.new("file.txt", "text/plain")
|
322
|
+
# UploadIO.new(file_io, "text/plain", "file.txt")
|
323
|
+
# ********************************************************************
|
324
|
+
#
|
325
|
+
# There is also a 4th undocumented argument, opts={}, which allows us
|
326
|
+
# to send in not only the Content-Disposition of 'file' as required by
|
327
|
+
# DocuSign, but also the documentId parameter which is required as well
|
328
|
+
#
|
329
|
+
ios = []
|
330
|
+
files.each_with_index do |file, index|
|
331
|
+
ios << UploadIO.new(
|
332
|
+
file[:io] || file[:path],
|
333
|
+
file[:content_type] || "application/pdf",
|
334
|
+
file[:name],
|
335
|
+
"Content-Disposition" => "file; documentid=#{index+1}"
|
336
|
+
)
|
337
|
+
end
|
338
|
+
ios
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
# Internal: sets up the file_params for inclusion in a multipart post request
|
343
|
+
#
|
344
|
+
# ios - An array of UploadIO formatted file objects
|
345
|
+
#
|
346
|
+
# Returns a hash of files params suitable for inclusion in a multipart
|
347
|
+
# post request
|
348
|
+
def create_file_params(ios)
|
349
|
+
# multi-doc uploading capabilities, each doc needs to be it's own param
|
350
|
+
file_params = {}
|
351
|
+
ios.each_with_index do |io,index|
|
352
|
+
file_params.merge!("file#{index+1}" => io)
|
353
|
+
end
|
354
|
+
file_params
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
# Internal: takes in an array of hashes of documents and calculates the
|
359
|
+
# documentId then concatenates all the hashes with commas
|
360
|
+
#
|
361
|
+
# Returns a hash of documents that are to be uploaded
|
362
|
+
def get_documents(ios)
|
363
|
+
documents = []
|
364
|
+
ios.each_with_index do |io, index|
|
365
|
+
documents << "{
|
366
|
+
\"documentId\" : \"#{index+1}\",
|
367
|
+
\"name\" : \"#{io.original_filename}\"
|
368
|
+
}"
|
369
|
+
end
|
370
|
+
documents.join(",")
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
# Internal sets up the Net::HTTP request
|
375
|
+
#
|
376
|
+
# uri - The fully qualified final URI
|
377
|
+
# post_body - The custom post body including the signers, etc
|
378
|
+
# file_params - Formatted hash of ios to merge into the post body
|
379
|
+
# headers - Allows for passing in custom headers
|
380
|
+
#
|
381
|
+
# Returns a request object suitable for embedding in a request
|
382
|
+
def initialize_net_http_multipart_post_request(uri, post_body, file_params, headers)
|
383
|
+
# Net::HTTP::Post::Multipart is from the multipart-post gem's lib/multipartable.rb
|
384
|
+
#
|
385
|
+
# path - The fully qualified URI for the request
|
386
|
+
# params - A hash of params (including files for uploading and a
|
387
|
+
# customized request body)
|
388
|
+
# headers={} - The fully merged, final request headers
|
389
|
+
# boundary - Optional: you can give the request a custom boundary
|
390
|
+
#
|
391
|
+
request = Net::HTTP::Post::Multipart.new(
|
392
|
+
uri.request_uri,
|
393
|
+
{post_body: post_body}.merge(file_params),
|
394
|
+
headers
|
395
|
+
)
|
396
|
+
|
397
|
+
# DocuSign requires that we embed the document data in the body of the
|
398
|
+
# JSON request directly so we need to call ".read" on the multipart-post
|
399
|
+
# provided body_stream in order to serialize all the files into a
|
400
|
+
# compatible JSON string.
|
401
|
+
request.body = request.body_stream.read
|
402
|
+
request
|
403
|
+
end
|
404
|
+
|
405
|
+
|
406
|
+
# Public: creates an envelope from a document directly without a template
|
407
|
+
#
|
408
|
+
# file_io - Optional: an opened file stream of data (if you don't
|
409
|
+
# want to save the file to the file system as an incremental
|
410
|
+
# step)
|
411
|
+
# file_path - Required if you don't provide a file_io stream, this is
|
412
|
+
# the local path of the file you wish to upload. Absolute
|
413
|
+
# paths recommended.
|
414
|
+
# file_name - The name you want to give to the file you are uploading
|
415
|
+
# content_type - (for the request body) application/json is what DocuSign
|
416
|
+
# is expecting
|
417
|
+
# email_subject - (Optional) short subject line for the email
|
418
|
+
# email_body - (Optional) custom text that will be injected into the
|
419
|
+
# DocuSign generated email
|
420
|
+
# signers - A hash of users who should receive the document and need
|
421
|
+
# to sign it. More info about the options available for
|
422
|
+
# this method are documented above it's method definition.
|
423
|
+
# status - Options include: 'sent', 'created', 'voided' and determine
|
424
|
+
# if the envelope is sent out immediately or stored for
|
425
|
+
# sending at a later time
|
426
|
+
# headers - Allows a client to pass in some
|
427
|
+
#
|
428
|
+
# Returns a response object containing:
|
429
|
+
# envelopeId - The envelope's ID
|
430
|
+
# status - Sent, created, or voided
|
431
|
+
# statusDateTime - The date/time the envelope was created
|
432
|
+
# uri - The relative envelope uri
|
433
|
+
def create_envelope_from_document(options={})
|
434
|
+
ios = create_file_ios(options[:files])
|
435
|
+
file_params = create_file_params(ios)
|
436
|
+
|
437
|
+
post_body = "{
|
438
|
+
\"emailBlurb\" : \"#{options[:email][:body] if options[:email]}\",
|
439
|
+
\"emailSubject\" : \"#{options[:email][:subject] if options[:email]}\",
|
440
|
+
\"documents\" : [#{get_documents(ios)}],
|
441
|
+
\"recipients\" : {
|
442
|
+
\"signers\" : [#{get_signers(options[:signers])}]
|
443
|
+
},
|
444
|
+
\"status\" : \"#{options[:status]}\"
|
445
|
+
}
|
446
|
+
"
|
447
|
+
|
448
|
+
uri = build_uri("/accounts/#{@acct_id}/envelopes")
|
449
|
+
|
450
|
+
http = initialize_net_http_ssl(uri)
|
451
|
+
|
452
|
+
request = initialize_net_http_multipart_post_request(
|
453
|
+
uri, post_body, file_params, headers(options[:headers])
|
454
|
+
)
|
455
|
+
|
456
|
+
# Finally do the Net::HTTP request!
|
457
|
+
http.request(request)
|
458
|
+
end
|
459
|
+
|
460
|
+
|
461
|
+
# Public: allows a template to be dynamically created with several options.
|
462
|
+
#
|
463
|
+
# files - An array of hashes of file parameters which will be used
|
464
|
+
# to create actual files suitable for upload in a multipart
|
465
|
+
# request.
|
466
|
+
#
|
467
|
+
# Options: io, path, name. The io is optional and would
|
468
|
+
# require creating a file_io object to embed as the first
|
469
|
+
# argument of any given file hash. See the create_file_ios
|
470
|
+
# method definition above for more details.
|
471
|
+
#
|
472
|
+
# email/body - (Optional) sets the text in the email. Note: the envelope
|
473
|
+
# seems to override this, not sure why it needs to be
|
474
|
+
# configured here as well. I usually leave it blank.
|
475
|
+
# email/subject - (Optional) sets the text in the email. Note: the envelope
|
476
|
+
# seems to override this, not sure why it needs to be
|
477
|
+
# configured here as well. I usually leave it blank.
|
478
|
+
# signers - An array of hashes of signers. See the
|
479
|
+
# get_template_signers method definition for options.
|
480
|
+
# description - The template description
|
481
|
+
# name - The template name
|
482
|
+
# headers - Optional hash of headers to merge into the existing
|
483
|
+
# required headers for a multipart request.
|
484
|
+
#
|
485
|
+
# Returns a response body containing the template's:
|
486
|
+
# name - Name given above
|
487
|
+
# templateId - The auto-generated ID provided by DocuSign
|
488
|
+
# Uri - the URI where the template is located on the DocuSign servers
|
489
|
+
def create_template(options={})
|
490
|
+
ios = create_file_ios(options[:files])
|
491
|
+
file_params = create_file_params(ios)
|
492
|
+
|
493
|
+
post_body = "{
|
494
|
+
\"emailBlurb\" : \"#{options[:email][:body] if options[:email]}\",
|
495
|
+
\"emailSubject\" : \"#{options[:email][:subject] if options[:email]}\",
|
496
|
+
\"documents\" : [#{get_documents(ios)}],
|
497
|
+
\"recipients\" : {
|
498
|
+
\"signers\" : [#{get_template_signers(options[:signers])}]
|
499
|
+
},
|
500
|
+
\"envelopeTemplateDefinition\" : {
|
501
|
+
\"description\" : \"#{options[:description]}\",
|
502
|
+
\"name\" : \"#{options[:name]}\",
|
503
|
+
\"pageCount\" : 1,
|
504
|
+
\"password\" : \"\",
|
505
|
+
\"shared\" : false
|
506
|
+
}
|
507
|
+
}
|
508
|
+
"
|
509
|
+
|
510
|
+
uri = build_uri("/accounts/#{@acct_id}/templates")
|
511
|
+
|
512
|
+
http = initialize_net_http_ssl(uri)
|
513
|
+
|
514
|
+
request = initialize_net_http_multipart_post_request(
|
515
|
+
uri, post_body, file_params, headers(options[:headers])
|
516
|
+
)
|
517
|
+
|
518
|
+
# Finally do the Net::HTTP request!
|
519
|
+
http.request(request)
|
520
|
+
end
|
521
|
+
|
522
|
+
|
523
|
+
# Public: create an envelope for delivery from a template
|
524
|
+
#
|
525
|
+
# headers - Optional hash of headers to merge into the existing
|
526
|
+
# required headers for a POST request.
|
527
|
+
# status - Options include: 'sent', 'created', 'voided' and
|
528
|
+
# determine if the envelope is sent out immediately or
|
529
|
+
# stored for sending at a later time
|
530
|
+
# email/body - Sets the text in the email body
|
531
|
+
# email/subject - Sets the text in the email subject line
|
532
|
+
# template_id - The id of the template upon which we want to base this
|
533
|
+
# envelope
|
534
|
+
# template_roles - See the get_template_roles method definition for a list
|
535
|
+
# of options to pass. Note: for consistency sake we call
|
536
|
+
# this 'signers' and not 'templateRoles' when we build up
|
537
|
+
# the request in client code.
|
538
|
+
# headers - Optional hash of headers to merge into the existing
|
539
|
+
# required headers for a multipart request.
|
540
|
+
#
|
541
|
+
# Returns a response body containing the envelope's:
|
542
|
+
# name - Name given above
|
543
|
+
# templateId - The auto-generated ID provided by DocuSign
|
544
|
+
# Uri - the URI where the template is located on the DocuSign servers
|
545
|
+
def create_envelope_from_template(options={})
|
546
|
+
content_type = {'Content-Type' => 'application/json'}
|
547
|
+
content_type.merge(options[:headers]) if options[:headers]
|
548
|
+
|
549
|
+
post_body = "{
|
550
|
+
\"status\" : \"#{options[:status]}\",
|
551
|
+
\"emailBlurb\" : \"#{options[:email][:body]}\",
|
552
|
+
\"emailSubject\" : \"#{options[:email][:subject]}\",
|
553
|
+
\"templateId\" : \"#{options[:template_id]}\",
|
554
|
+
\"templateRoles\" : [#{get_template_roles(options[:signers])}],
|
555
|
+
}"
|
556
|
+
|
557
|
+
uri = build_uri("/accounts/#{@acct_id}/envelopes")
|
558
|
+
|
559
|
+
http = initialize_net_http_ssl(uri)
|
560
|
+
|
561
|
+
request = Net::HTTP::Post.new(
|
562
|
+
uri.request_uri,
|
563
|
+
headers(content_type)
|
564
|
+
)
|
565
|
+
request.body = post_body
|
566
|
+
|
567
|
+
http.request(request)
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
# Public returns the URL for embedded signing
|
572
|
+
#
|
573
|
+
# envelope_id - the ID of the envelope you wish to use for embedded signing
|
574
|
+
# name - the name of the signer
|
575
|
+
# email - the email of the recipient
|
576
|
+
# return_url - the URL you want the user to be directed to after he or she
|
577
|
+
# completes the document signing
|
578
|
+
# headers - optional hash of headers to merge into the existing
|
579
|
+
# required headers for a multipart request.
|
580
|
+
#
|
581
|
+
# Returns the URL for embedded signing which needs to be put in an iFrame
|
582
|
+
def get_recipient_view(options={})
|
583
|
+
content_type = {'Content-Type' => 'application/json'}
|
584
|
+
content_type.merge(options[:headers]) if options[:headers]
|
585
|
+
|
586
|
+
post_body = "{
|
587
|
+
\"authenticationMethod\" : \"email\",
|
588
|
+
\"clientUserId\" : \"#{options[:email]}\",
|
589
|
+
\"email\" : \"#{options[:email]}\",
|
590
|
+
\"returnUrl\" : \"#{options[:return_url]}\",
|
591
|
+
\"userName\" : \"#{options[:name]}\",
|
592
|
+
}"
|
593
|
+
|
594
|
+
uri = build_uri("/accounts/#{@acct_id}/envelopes/#{options[:envelope_id]}/views/recipient")
|
595
|
+
|
596
|
+
http = initialize_net_http_ssl(uri)
|
597
|
+
|
598
|
+
request = Net::HTTP::Post.new(
|
599
|
+
uri.request_uri,
|
600
|
+
headers(content_type)
|
601
|
+
)
|
602
|
+
request.body = post_body
|
603
|
+
|
604
|
+
http.request(request)
|
605
|
+
end
|
606
|
+
|
607
|
+
end
|
608
|
+
|
609
|
+
end
|