allscripts_unity_client 1.0.3
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.
- data/.gitignore +20 -0
- data/.travis.yml +3 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +180 -0
- data/Rakefile +7 -0
- data/allscripts_unity_client.gemspec +39 -0
- data/lib/allscripts_unity_client.rb +43 -0
- data/lib/allscripts_unity_client/client.rb +594 -0
- data/lib/allscripts_unity_client/client_driver.rb +95 -0
- data/lib/allscripts_unity_client/json_client_driver.rb +110 -0
- data/lib/allscripts_unity_client/json_unity_request.rb +33 -0
- data/lib/allscripts_unity_client/json_unity_response.rb +27 -0
- data/lib/allscripts_unity_client/soap_client_driver.rb +128 -0
- data/lib/allscripts_unity_client/timezone.rb +99 -0
- data/lib/allscripts_unity_client/unity_request.rb +63 -0
- data/lib/allscripts_unity_client/unity_response.rb +110 -0
- data/lib/allscripts_unity_client/utilities.rb +66 -0
- data/lib/allscripts_unity_client/version.rb +3 -0
- data/spec/allscripts_unity_client_spec.rb +57 -0
- data/spec/client_driver_spec.rb +71 -0
- data/spec/client_spec.rb +406 -0
- data/spec/factories/allscripts_unity_client_parameters_factory.rb +13 -0
- data/spec/factories/client_driver_factory.rb +14 -0
- data/spec/factories/client_factory.rb +7 -0
- data/spec/factories/json_client_driver_factory.rb +3 -0
- data/spec/factories/json_unity_request_factory.rb +3 -0
- data/spec/factories/json_unity_response_factory.rb +3 -0
- data/spec/factories/magic_request_factory.rb +33 -0
- data/spec/factories/soap_client_driver_factory.rb +3 -0
- data/spec/factories/timezone_factory.rb +7 -0
- data/spec/factories/unity_request_factory.rb +10 -0
- data/spec/factories/unity_response_factory.rb +8 -0
- data/spec/fixtures/attributes_hash.yml +15 -0
- data/spec/fixtures/date_hash.yml +8 -0
- data/spec/fixtures/date_string_hash.yml +8 -0
- data/spec/fixtures/error.json +3 -0
- data/spec/fixtures/get_providers.json +69 -0
- data/spec/fixtures/get_providers.xml +119 -0
- data/spec/fixtures/get_providers_json.yml +65 -0
- data/spec/fixtures/get_providers_xml.yml +270 -0
- data/spec/fixtures/get_security_token.json +1 -0
- data/spec/fixtures/get_security_token.xml +7 -0
- data/spec/fixtures/get_server_info.json +10 -0
- data/spec/fixtures/get_server_info.xml +40 -0
- data/spec/fixtures/get_server_info_json.yml +8 -0
- data/spec/fixtures/get_server_info_xml.yml +55 -0
- data/spec/fixtures/no_attributes_hash.yml +7 -0
- data/spec/fixtures/retire_security_token.json +1 -0
- data/spec/fixtures/retire_security_token.xml +5 -0
- data/spec/fixtures/soap_fault.xml +13 -0
- data/spec/fixtures/string_keyed_hash.yml +8 -0
- data/spec/fixtures/symbol_keyed_hash.yml +8 -0
- data/spec/json_client_driver_spec.rb +209 -0
- data/spec/json_unity_request_spec.rb +37 -0
- data/spec/json_unity_response_spec.rb +44 -0
- data/spec/soap_client_driver_spec.rb +201 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/fixture_loader.rb +22 -0
- data/spec/support/shared_examples_for_client_driver.rb +139 -0
- data/spec/support/shared_examples_for_unity_request.rb +94 -0
- data/spec/support/shared_examples_for_unity_response.rb +26 -0
- data/spec/timezone_spec.rb +161 -0
- data/spec/unity_request_spec.rb +37 -0
- data/spec/unity_response_spec.rb +36 -0
- data/spec/utilities_spec.rb +69 -0
- metadata +323 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module AllscriptsUnityClient
|
4
|
+
class ClientDriver
|
5
|
+
LOG_FILE = "logs/unity_client.log"
|
6
|
+
|
7
|
+
attr_accessor :username, :password, :appname, :base_unity_url, :proxy, :security_token, :timezone, :logger, :log
|
8
|
+
|
9
|
+
def initialize(base_unity_url, username, password, appname, proxy = nil, timezone = nil, logger = nil, log = true)
|
10
|
+
raise ArgumentError, "base_unity_url can not be nil" if base_unity_url.nil?
|
11
|
+
raise ArgumentError, "username can not be nil" if username.nil?
|
12
|
+
raise ArgumentError, "password can not be nil" if password.nil?
|
13
|
+
raise ArgumentError, "appname can not be nil" if appname.nil?
|
14
|
+
|
15
|
+
@base_unity_url = base_unity_url.gsub /\/$/, ""
|
16
|
+
@username = username
|
17
|
+
@password = password
|
18
|
+
@appname = appname
|
19
|
+
@proxy = proxy
|
20
|
+
@log = log
|
21
|
+
|
22
|
+
if logger.nil?
|
23
|
+
@logger = Logger.new(STDOUT)
|
24
|
+
@logger.level = Logger::INFO
|
25
|
+
else
|
26
|
+
@logger = logger
|
27
|
+
end
|
28
|
+
|
29
|
+
unless timezone.nil?
|
30
|
+
@timezone = Timezone.new(timezone)
|
31
|
+
else
|
32
|
+
@timezone = Timezone.new("UTC")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def security_token?
|
37
|
+
return !@security_token.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
def log?
|
41
|
+
return @log
|
42
|
+
end
|
43
|
+
|
44
|
+
def client_type
|
45
|
+
return :none
|
46
|
+
end
|
47
|
+
|
48
|
+
def magic(parameters = {})
|
49
|
+
raise NotImplementedError, "magic not implemented"
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_security_token!(parameters = {})
|
53
|
+
raise NotImplementedError, "get_security_token! not implemented"
|
54
|
+
end
|
55
|
+
|
56
|
+
def retire_security_token!(parameters = {})
|
57
|
+
raise NotImplementedError, "retire_security_token! not implemented"
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def log_get_security_token
|
63
|
+
message = "Unity API GetSecurityToken request to #{@base_unity_url}"
|
64
|
+
log_info(message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def log_retire_security_token
|
68
|
+
message = "Unity API RetireSecurityToken request to #{@base_unity_url}"
|
69
|
+
log_info(message)
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_magic(request)
|
73
|
+
raise ArgumentError, "request can not be nil" if request.nil?
|
74
|
+
message = "Unity API Magic request to #{@base_unity_url} [#{request.parameters[:action]}]"
|
75
|
+
log_info(message)
|
76
|
+
end
|
77
|
+
|
78
|
+
def log_info(message)
|
79
|
+
if log? && !logger.nil? && !message.nil?
|
80
|
+
message += " #{@timer} seconds" unless @timer.nil?
|
81
|
+
@timer = nil
|
82
|
+
logger.info(message)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def start_timer
|
87
|
+
@start_time = Time.now.utc
|
88
|
+
end
|
89
|
+
|
90
|
+
def end_timer
|
91
|
+
@end_time = Time.now.utc
|
92
|
+
@timer = @end_time - @start_time
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "json"
|
2
|
+
require "httpi"
|
3
|
+
|
4
|
+
module AllscriptsUnityClient
|
5
|
+
class JSONClientDriver < ClientDriver
|
6
|
+
attr_accessor :json_base_url
|
7
|
+
|
8
|
+
UNITY_JSON_ENDPOINT = "/Unity/UnityService.svc/json"
|
9
|
+
|
10
|
+
def initialize(base_unity_url, username, password, appname, proxy = nil, timezone = nil, logger = nil, log = true)
|
11
|
+
super
|
12
|
+
|
13
|
+
# Disable HTTPI logging
|
14
|
+
HTTPI.log = false
|
15
|
+
|
16
|
+
@json_base_url = "#{@base_unity_url}#{UNITY_JSON_ENDPOINT}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def client_type
|
20
|
+
return :json
|
21
|
+
end
|
22
|
+
|
23
|
+
def magic(parameters = {})
|
24
|
+
request_data = JSONUnityRequest.new(parameters, @timezone, @appname, @security_token)
|
25
|
+
request = create_httpi_request("#{@json_base_url}/MagicJson", request_data.to_hash)
|
26
|
+
|
27
|
+
start_timer
|
28
|
+
response = HTTPI.post(request)
|
29
|
+
end_timer
|
30
|
+
|
31
|
+
response = JSON.parse(response.body)
|
32
|
+
|
33
|
+
raise_if_response_error(response)
|
34
|
+
log_magic(request_data)
|
35
|
+
|
36
|
+
response = JSONUnityResponse.new(response, @timezone)
|
37
|
+
response.to_hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_security_token!(parameters = {})
|
41
|
+
username = parameters[:username] || @username
|
42
|
+
password = parameters[:password] || @password
|
43
|
+
appname = parameters[:appname] || @appname
|
44
|
+
|
45
|
+
request_data = {
|
46
|
+
"Username" => username,
|
47
|
+
"Password" => password,
|
48
|
+
"Appname" => appname
|
49
|
+
}
|
50
|
+
request = create_httpi_request("#{@json_base_url}/GetToken", request_data)
|
51
|
+
|
52
|
+
start_timer
|
53
|
+
response = HTTPI.post(request, :net_http_persistent)
|
54
|
+
end_timer
|
55
|
+
|
56
|
+
raise_if_response_error(response.body)
|
57
|
+
log_get_security_token
|
58
|
+
|
59
|
+
@security_token = response.body
|
60
|
+
end
|
61
|
+
|
62
|
+
def retire_security_token!(parameters = {})
|
63
|
+
token = parameters[:token] || @security_token
|
64
|
+
appname = parameters[:appname] || @appname
|
65
|
+
|
66
|
+
request_data = {
|
67
|
+
"Token" => token,
|
68
|
+
"Appname" => appname
|
69
|
+
}
|
70
|
+
request = create_httpi_request("#{@json_base_url}/RetireSecurityToken", request_data)
|
71
|
+
|
72
|
+
start_timer
|
73
|
+
response = HTTPI.post(request, :net_http_persistent)
|
74
|
+
end_timer
|
75
|
+
|
76
|
+
raise_if_response_error(response.body)
|
77
|
+
log_retire_security_token
|
78
|
+
|
79
|
+
@security_token = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def create_httpi_request(url, data)
|
85
|
+
request = HTTPI::Request.new
|
86
|
+
request.url = url
|
87
|
+
request.headers = {
|
88
|
+
"Accept-Encoding" => "gzip,deflate",
|
89
|
+
"Content-type" => "application/json;charset=utf-8"
|
90
|
+
}
|
91
|
+
request.body = JSON.generate(data)
|
92
|
+
|
93
|
+
unless @proxy.nil?
|
94
|
+
request.proxy = @proxy
|
95
|
+
end
|
96
|
+
|
97
|
+
request
|
98
|
+
end
|
99
|
+
|
100
|
+
def raise_if_response_error(response)
|
101
|
+
if response.nil?
|
102
|
+
raise APIError, "Response was empty"
|
103
|
+
elsif response.is_a?(Array) && !response[0]["Error"].nil?
|
104
|
+
raise APIError, response[0]["Error"]
|
105
|
+
elsif response.is_a?(String) && response.include?("error:")
|
106
|
+
raise APIError, response
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AllscriptsUnityClient
|
2
|
+
class JSONUnityRequest < UnityRequest
|
3
|
+
def to_hash
|
4
|
+
action = @parameters[:action]
|
5
|
+
userid = @parameters[:userid]
|
6
|
+
appname = @parameters[:appname] || @appname
|
7
|
+
patientid = @parameters[:patientid]
|
8
|
+
token = @parameters[:token] || @security_token
|
9
|
+
parameter1 = process_date(@parameters[:parameter1]) || ""
|
10
|
+
parameter2 = process_date(@parameters[:parameter2]) || ""
|
11
|
+
parameter3 = process_date(@parameters[:parameter3]) || ""
|
12
|
+
parameter4 = process_date(@parameters[:parameter4]) || ""
|
13
|
+
parameter5 = process_date(@parameters[:parameter5]) || ""
|
14
|
+
parameter6 = process_date(@parameters[:parameter6]) || ""
|
15
|
+
data = Utilities::encode_data(@parameters[:data]) || ""
|
16
|
+
|
17
|
+
return {
|
18
|
+
"Action" => action,
|
19
|
+
"AppUserID" => userid,
|
20
|
+
"Appname" => appname,
|
21
|
+
"PatientID" => patientid,
|
22
|
+
"Token" => token,
|
23
|
+
"Parameter1" => parameter1,
|
24
|
+
"Parameter2" => parameter2,
|
25
|
+
"Parameter3" => parameter3,
|
26
|
+
"Parameter4" => parameter4,
|
27
|
+
"Parameter5" => parameter5,
|
28
|
+
"Parameter6" => parameter6,
|
29
|
+
"Data" => data
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module AllscriptsUnityClient
|
4
|
+
class JSONUnityResponse < UnityResponse
|
5
|
+
def to_hash
|
6
|
+
result = @response
|
7
|
+
|
8
|
+
# All JSON magic responses are an array with one item
|
9
|
+
result = result.first
|
10
|
+
|
11
|
+
# All JSON magic results contain one key on their object named
|
12
|
+
# actioninfo
|
13
|
+
result = result.values.first
|
14
|
+
|
15
|
+
# The data in a JSON magic result is always an array. If that array
|
16
|
+
# only has a single item, then just return that as the result. This is
|
17
|
+
# a compromise as some actions that should always return arrays
|
18
|
+
# (i.e. GetProviders) may return a single hash.
|
19
|
+
if result.count == 1
|
20
|
+
result = result.first
|
21
|
+
end
|
22
|
+
|
23
|
+
result = convert_dates_to_utc(result)
|
24
|
+
Utilities::recursively_symbolize_keys(result)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require "savon"
|
2
|
+
|
3
|
+
module AllscriptsUnityClient
|
4
|
+
class SOAPClientDriver < ClientDriver
|
5
|
+
attr_accessor :savon_client
|
6
|
+
|
7
|
+
UNITY_SOAP_ENDPOINT = "/Unity/UnityService.svc/unityservice"
|
8
|
+
UNITY_ENDPOINT_NAMESPACE = "http://www.allscripts.com/Unity/IUnityService"
|
9
|
+
|
10
|
+
def initialize(base_unity_url, username, password, appname, proxy = nil, timezone = nil, logger = nil, log = true)
|
11
|
+
super
|
12
|
+
|
13
|
+
client_proxy = @proxy
|
14
|
+
base_unity_url = "#{@base_unity_url}#{UNITY_SOAP_ENDPOINT}"
|
15
|
+
|
16
|
+
@savon_client = Savon.client do
|
17
|
+
# Removes the wsdl: namespace from body elements in the SOAP
|
18
|
+
# request. Unity doesn't recognize elements otherwise.
|
19
|
+
namespace_identifier nil
|
20
|
+
|
21
|
+
# Manually registers SOAP endpoint since Unity WSDL is not very
|
22
|
+
# good.
|
23
|
+
endpoint base_unity_url
|
24
|
+
|
25
|
+
# Manually register SOAP namespace. This URL isn't live, but the
|
26
|
+
# internal SOAP endpoints expect it.
|
27
|
+
namespace "http://www.allscripts.com/Unity"
|
28
|
+
|
29
|
+
# Register proxy with Savon if one was given.
|
30
|
+
unless client_proxy.nil?
|
31
|
+
proxy client_proxy
|
32
|
+
end
|
33
|
+
|
34
|
+
# Unity expects the SOAP envelop to be namespaced with soap:
|
35
|
+
env_namespace :soap
|
36
|
+
|
37
|
+
# Unity uses Microsoft style CamelCase for keys. Only really useful when using
|
38
|
+
# symbol keyed hashes.
|
39
|
+
convert_request_keys_to :camelcase
|
40
|
+
|
41
|
+
# Enable gzip on HTTP responses. Unity does not currently support this
|
42
|
+
# as of Born On 10/7/2013, but it doesn't hurt to future-proof. If gzip
|
43
|
+
# is ever enabled, this library will get a speed bump for free.
|
44
|
+
headers({ "Accept-Encoding" => "gzip,deflate" })
|
45
|
+
|
46
|
+
# Disable Savon logs
|
47
|
+
log false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def client_type
|
52
|
+
return :soap
|
53
|
+
end
|
54
|
+
|
55
|
+
def magic(parameters = {})
|
56
|
+
request_data = UnityRequest.new(parameters, @timezone, @appname, @security_token)
|
57
|
+
call_data = {
|
58
|
+
:message => request_data.to_hash,
|
59
|
+
:soap_action => "#{UNITY_ENDPOINT_NAMESPACE}/Magic"
|
60
|
+
}
|
61
|
+
|
62
|
+
begin
|
63
|
+
start_timer
|
64
|
+
response = @savon_client.call("Magic", call_data)
|
65
|
+
end_timer
|
66
|
+
rescue Savon::SOAPFault => e
|
67
|
+
raise APIError, e.message
|
68
|
+
end
|
69
|
+
|
70
|
+
log_magic(request_data)
|
71
|
+
|
72
|
+
response = UnityResponse.new(response.body, @timezone)
|
73
|
+
response.to_hash
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_security_token!(parameters = {})
|
77
|
+
username = parameters[:username] || @username
|
78
|
+
password = parameters[:password] || @password
|
79
|
+
appname = parameters[:appname] || @appname
|
80
|
+
|
81
|
+
call_data = {
|
82
|
+
:message => {
|
83
|
+
"Username" => username,
|
84
|
+
"Password" => password,
|
85
|
+
"Appname" => appname
|
86
|
+
},
|
87
|
+
:soap_action => "#{UNITY_ENDPOINT_NAMESPACE}/GetSecurityToken"
|
88
|
+
}
|
89
|
+
|
90
|
+
begin
|
91
|
+
start_timer
|
92
|
+
response = @savon_client.call("GetSecurityToken", call_data)
|
93
|
+
end_timer
|
94
|
+
rescue Savon::SOAPFault => e
|
95
|
+
raise APIError, e.message
|
96
|
+
end
|
97
|
+
|
98
|
+
log_get_security_token
|
99
|
+
|
100
|
+
@security_token = response.body[:get_security_token_response][:get_security_token_result]
|
101
|
+
end
|
102
|
+
|
103
|
+
def retire_security_token!(parameters = {})
|
104
|
+
token = parameters[:token] || @security_token
|
105
|
+
appname = parameters[:appname] || @appname
|
106
|
+
|
107
|
+
call_data = {
|
108
|
+
:message => {
|
109
|
+
"Token" => token,
|
110
|
+
"Appname" => appname
|
111
|
+
},
|
112
|
+
:soap_action => "#{UNITY_ENDPOINT_NAMESPACE}/RetireSecurityToken"
|
113
|
+
}
|
114
|
+
|
115
|
+
begin
|
116
|
+
start_timer
|
117
|
+
@savon_client.call("RetireSecurityToken", call_data)
|
118
|
+
end_timer
|
119
|
+
rescue Savon::SOAPFault => e
|
120
|
+
raise APIError, e.message
|
121
|
+
end
|
122
|
+
|
123
|
+
log_retire_security_token
|
124
|
+
|
125
|
+
@security_token = nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require "date"
|
2
|
+
require "tzinfo"
|
3
|
+
|
4
|
+
module AllscriptsUnityClient
|
5
|
+
class Timezone
|
6
|
+
attr_accessor :tzinfo
|
7
|
+
|
8
|
+
def initialize(zone_identifier)
|
9
|
+
raise ArgumentError, "zone_identifier can not be nil" if zone_identifier.nil?
|
10
|
+
|
11
|
+
@tzinfo = TZInfo::Timezone.get(zone_identifier)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Use TZInfo to convert a given UTC datetime into
|
15
|
+
# a local
|
16
|
+
def local_to_utc(datetime)
|
17
|
+
convert_with_timezone(:local_to_utc, datetime)
|
18
|
+
end
|
19
|
+
|
20
|
+
def utc_to_local(datetime = nil)
|
21
|
+
convert_with_timezone(:utc_to_local, datetime)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Direction can be :utc_to_local or :local_to_utc
|
27
|
+
def convert_with_timezone(direction, datetime = nil)
|
28
|
+
if datetime.nil?
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# Dates have no time information and so timezone conversion
|
33
|
+
# doesn't make sense. Just return the date in this case.
|
34
|
+
if datetime.instance_of?(Date)
|
35
|
+
return datetime
|
36
|
+
end
|
37
|
+
|
38
|
+
if datetime.instance_of?(String)
|
39
|
+
datetime = DateTime.parse(datetime.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
is_datetime = datetime.instance_of?(DateTime)
|
43
|
+
|
44
|
+
if direction == :local_to_utc
|
45
|
+
if is_datetime
|
46
|
+
# DateTime can do UTC conversions reliably, so use that instead of
|
47
|
+
# TZInfo
|
48
|
+
datetime = datetime.new_offset(0)
|
49
|
+
else
|
50
|
+
datetime = @tzinfo.local_to_utc(datetime)
|
51
|
+
end
|
52
|
+
|
53
|
+
if is_datetime
|
54
|
+
# Convert to a DateTime with a UTC offset
|
55
|
+
datetime = DateTime.parse("#{datetime.strftime("%FT%T")}Z")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
if direction == :utc_to_local
|
60
|
+
datetime = @tzinfo.utc_to_local(datetime)
|
61
|
+
|
62
|
+
if is_datetime
|
63
|
+
# Convert to a DateTime with the correct timezone offset
|
64
|
+
datetime = DateTime.parse(iso8601_with_offset(datetime))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
return datetime
|
69
|
+
end
|
70
|
+
|
71
|
+
# TZInfo does not correctly update a DateTime's
|
72
|
+
# offset, so we manually format a ISO8601 with
|
73
|
+
# the correct format
|
74
|
+
def iso8601_with_offset(datetime)
|
75
|
+
if datetime.nil?
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
|
79
|
+
offset = @tzinfo.current_period.utc_offset
|
80
|
+
negative_offset = false
|
81
|
+
datetime_string = datetime.strftime("%FT%T")
|
82
|
+
|
83
|
+
if offset < 0
|
84
|
+
offset *= -1
|
85
|
+
negative_offset = true
|
86
|
+
end
|
87
|
+
|
88
|
+
if offset == 0
|
89
|
+
offset_string = "Z"
|
90
|
+
else
|
91
|
+
offset_string = Time.at(offset).utc.strftime("%H:%M")
|
92
|
+
offset_string = "-" + offset_string if negative_offset
|
93
|
+
offset_string = "+" + offset_string unless negative_offset
|
94
|
+
end
|
95
|
+
|
96
|
+
"#{datetime_string}#{offset_string}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|