tender_import 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +18 -0
- data/bin/zendesk2tender +22 -0
- data/lib/tender_import/archive.rb +216 -0
- data/lib/tender_import/version.rb +4 -0
- data/lib/tender_import/zendesk_api_import.rb +307 -0
- data/lib/tender_import.rb +4 -0
- metadata +119 -0
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Tender Import Scripts
|
2
|
+
|
3
|
+
This is a repository of code for producing [Tender import
|
4
|
+
archives](https://help.tenderapp.com/faqs/setup-installation/importing),
|
5
|
+
for Tender customers who wish to move their discussions from another service.
|
6
|
+
|
7
|
+
The methods used to produce an import archive will vary with the service
|
8
|
+
and the facilities they provide, such as data dumps or API access. These
|
9
|
+
scripts are being made available in the hopes that they'll be useful, but
|
10
|
+
it's up to you to review them before running them on your own computer or
|
11
|
+
against an existing service. We aren't responsible if your computer blows
|
12
|
+
up or you are banned from an existing service for TOS violations, or both.
|
13
|
+
|
14
|
+
That said, if you make useful or necessary modifications to a script, or
|
15
|
+
produce a script for importing from a new service, please
|
16
|
+
[open a Tender discussion](https://help.tenderapp.com/discussions/suggestions#new_topic_form)
|
17
|
+
or send a pull request to let us know.
|
18
|
+
|
data/bin/zendesk2tender
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems -Ilib
|
2
|
+
#
|
3
|
+
# Produce a Tender import archive by collecting tickets and discussions from
|
4
|
+
# the ZenDesk API. Requires the ZenDesk subdomain and login credentials.
|
5
|
+
#
|
6
|
+
# For more info: https://help.tenderapp.com/faqs/setup-installation/importing
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
# zendesk2tender -e <email> -p <password> -s <subdomain>
|
10
|
+
#
|
11
|
+
# `zendesk2tender --help' displays detailed option info.
|
12
|
+
#
|
13
|
+
# Prerequisites:
|
14
|
+
# # Ruby gems
|
15
|
+
# gem install faraday -v "~>0.4.5"
|
16
|
+
# gem install trollop
|
17
|
+
# gem install yajl-ruby
|
18
|
+
# # Python tools (must be in your PATH)
|
19
|
+
# html2text.py: # https://github.com/aaronsw/html2text
|
20
|
+
#
|
21
|
+
require 'tender_import'
|
22
|
+
TenderImport::ZendeskApiImport.run
|
@@ -0,0 +1,216 @@
|
|
1
|
+
#
|
2
|
+
# Provides a Ruby API for constructing Tender import archives.
|
3
|
+
#
|
4
|
+
# https://help.tenderapp.com/faqs/setup-installation/importing
|
5
|
+
#
|
6
|
+
# ## Example
|
7
|
+
#
|
8
|
+
# # Currently requires a site name.
|
9
|
+
# archive = TenderImport::Archive.new('tacotown')
|
10
|
+
#
|
11
|
+
# # Can add users, categories and discussions to the archive.
|
12
|
+
# archive.add_user :email => 'frank@tacotown.com', :state => 'support'
|
13
|
+
# archive.add_user :email => 'bob@bobfoo.com'
|
14
|
+
#
|
15
|
+
# # When you add a category you'll get a handle needed to add discusions.
|
16
|
+
# category = archive.add_category :name => 'Tacos'
|
17
|
+
#
|
18
|
+
# # Discussions must have at least one comment.
|
19
|
+
# archive.add_discussion category, :title => 'your tacos',
|
20
|
+
# :author_email => 'bob@bobfoo.com',
|
21
|
+
# :comments => [{
|
22
|
+
# :author_email => 'bob@bobfoo.com',
|
23
|
+
# :body => 'They are not so good.'
|
24
|
+
# }, {
|
25
|
+
# :author_email => 'frank@tacotown.com',
|
26
|
+
# :body => 'You have terrible taste in tacos. Good day, sir.'
|
27
|
+
# }]
|
28
|
+
#
|
29
|
+
# # By default, files are written as you add them, so this will just
|
30
|
+
# # assemble a gzipped tar archive from those files.
|
31
|
+
# filename = archive.write_archive
|
32
|
+
# puts "your import file is #{filename}"
|
33
|
+
#
|
34
|
+
# # If any errors are reported, some records were not included in the archive.
|
35
|
+
# if !archive.report.empty?
|
36
|
+
# puts "Problems reported: ", *archive.report
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
require 'yajl'
|
40
|
+
require 'fileutils'
|
41
|
+
class TenderImport::Archive
|
42
|
+
class Error < StandardError; end
|
43
|
+
include FileUtils
|
44
|
+
attr_reader :site, :report, :stats, :buffer
|
45
|
+
|
46
|
+
# Options:
|
47
|
+
#
|
48
|
+
# buffer:: When true, don't flush to disk until the end. Defaults to false.
|
49
|
+
#
|
50
|
+
def initialize site_name, options = {}
|
51
|
+
@site = site_name
|
52
|
+
@export_dir = ".#{site_name}-export-#{$$}"
|
53
|
+
@report = []
|
54
|
+
@import = {}
|
55
|
+
@stats = {}
|
56
|
+
@buffer = options.key?(:buffer) ? !!options[:buffer] : false
|
57
|
+
@category_counter = Hash.new(0)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the params on success, nil on failure
|
61
|
+
def add_user params
|
62
|
+
validate_and_store :user, {:state => 'user'}.merge(params)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns a handle needed for adding discussions
|
66
|
+
def add_category params
|
67
|
+
cat = validate_and_store :category, params
|
68
|
+
cat ? category_key(cat) : nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def add_discussion category_key, params
|
72
|
+
raise Error, "add_discussion: missing category key" if category_key.nil?
|
73
|
+
validate_and_store :discussion, params, :key => category_key
|
74
|
+
end
|
75
|
+
|
76
|
+
def category_key cat
|
77
|
+
"category:#{category_id cat}".downcase
|
78
|
+
end
|
79
|
+
|
80
|
+
def category_id cat
|
81
|
+
cat[:name].gsub(/\W+/,'_').downcase
|
82
|
+
end
|
83
|
+
|
84
|
+
def categories
|
85
|
+
@import[:category]
|
86
|
+
end
|
87
|
+
|
88
|
+
def discussions category_key
|
89
|
+
raise Error, "discussions: missing category key" if category_key.nil?
|
90
|
+
@import[category_key] || []
|
91
|
+
end
|
92
|
+
|
93
|
+
def users
|
94
|
+
@import[:user]
|
95
|
+
end
|
96
|
+
|
97
|
+
def write_archive
|
98
|
+
write_users if users
|
99
|
+
write_categories_and_discussions if categories
|
100
|
+
export_file = "export_#{site}.tgz"
|
101
|
+
system "tar -zcf #{export_file} -C #{export_dir} ."
|
102
|
+
system "rm -rf #{export_dir}"
|
103
|
+
return export_file
|
104
|
+
end
|
105
|
+
|
106
|
+
def write_user user
|
107
|
+
return unless user
|
108
|
+
mkdir_p export_dir('users')
|
109
|
+
File.open(File.join(export_dir('users'), "#{user[:email].gsub(/\W+/,'_')}.json"), "w") do |file|
|
110
|
+
file.puts Yajl::Encoder.encode(user)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def write_users
|
115
|
+
users.each do |u|
|
116
|
+
write_user u
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def write_category c
|
121
|
+
mkdir_p export_dir('categories')
|
122
|
+
File.open(File.join(export_dir('categories'), "#{category_id(c)}.json"), "w") do |file|
|
123
|
+
file.puts Yajl::Encoder.encode(c)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def write_categories_and_discussions
|
128
|
+
categories.each do |c|
|
129
|
+
write_category c
|
130
|
+
write_discussions c
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def write_discussion category_id, discussion
|
135
|
+
@category_counter[category_id] += 1
|
136
|
+
dir = File.join(export_dir('categories'), category_id)
|
137
|
+
mkdir_p dir
|
138
|
+
File.open(File.join(dir, "#{@category_counter[category_id]}.json"), "w") do |file|
|
139
|
+
file.puts Yajl::Encoder.encode(discussion)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def write_discussions category
|
144
|
+
discussions(category_key(category)).each do |d|
|
145
|
+
write_discussion category_id(category), d
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
|
151
|
+
def validate_and_store *args
|
152
|
+
type, params, options = args
|
153
|
+
options ||= {}
|
154
|
+
key = options[:key] || type
|
155
|
+
@import[key] ||= []
|
156
|
+
if valid? type, params
|
157
|
+
if buffer
|
158
|
+
# save in memory and flush to disk at the end
|
159
|
+
@import[key] << params
|
160
|
+
else
|
161
|
+
# write immediately instead of storing in memory
|
162
|
+
write *args
|
163
|
+
end
|
164
|
+
@stats[key] ||= 0
|
165
|
+
@stats[key] += 1
|
166
|
+
params
|
167
|
+
else
|
168
|
+
@stats["invalid:#{key}"] ||= 0
|
169
|
+
@stats["invalid:#{key}"] += 1
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def write type, params, options = {}
|
175
|
+
case type
|
176
|
+
when :discussion
|
177
|
+
# ughh
|
178
|
+
write_discussion options[:key].split(':',2)[1], params
|
179
|
+
when :category
|
180
|
+
write_category params
|
181
|
+
when :user
|
182
|
+
write_user params
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def valid? type, params
|
187
|
+
problems = []
|
188
|
+
# XXX this is not really enough validation, also it's ugly as fuck
|
189
|
+
if type == :user && (params[:email].nil? || params[:email].empty?)
|
190
|
+
problems << "Missing email in user data: #{params.inspect}."
|
191
|
+
end
|
192
|
+
if type == :user && !%w[user support].include?(params[:state])
|
193
|
+
problems << "Invalid state in user data: #{params.inspect}."
|
194
|
+
end
|
195
|
+
if type == :category && (params[:name].nil? || params[:name].empty?)
|
196
|
+
problems << "Missing name in category data: #{params.inspect}."
|
197
|
+
end
|
198
|
+
if type == :discussion && (params[:author_email].nil? || params[:author_email].empty?)
|
199
|
+
problems << "Missing author_email in discussion data: #{params.inspect}."
|
200
|
+
end
|
201
|
+
if type == :discussion && (params[:comments].nil? || params[:comments].any? {|c| c[:author_email].nil? || c[:author_email].empty?})
|
202
|
+
problems << "Missing comments and authors in discussion data: #{params.inspect}."
|
203
|
+
end
|
204
|
+
if problems.empty?
|
205
|
+
true
|
206
|
+
else
|
207
|
+
@report += problems
|
208
|
+
false
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def export_dir subdir=nil
|
213
|
+
subdir.nil? ? @export_dir : File.join(@export_dir, subdir.to_s)
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
require 'faraday'
|
3
|
+
require 'trollop'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
# Produce a Tender import archive from a ZenDesk site using the ZenDesk API.
|
8
|
+
class TenderImport::ZendeskApiImport
|
9
|
+
class Error < StandardError; end
|
10
|
+
class ResponseJSON < Faraday::Response::Middleware
|
11
|
+
def parse(body)
|
12
|
+
Yajl::Parser.parse(body)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Log # {{{
|
17
|
+
attr_reader :logger
|
18
|
+
def log string
|
19
|
+
logger.info "#{to_s}: #{string}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def debug string
|
23
|
+
logger.debug "#{to_s}: #{string}"
|
24
|
+
end
|
25
|
+
end # }}}
|
26
|
+
|
27
|
+
class Client # {{{
|
28
|
+
include Log
|
29
|
+
attr_reader :opts, :conn, :subdomain
|
30
|
+
|
31
|
+
# If no options are provided they will be obtained from the command-line.
|
32
|
+
#
|
33
|
+
# The options are subdomain, email and password.
|
34
|
+
#
|
35
|
+
# There is also an optional logger option, for use with Ruby/Rails
|
36
|
+
def initialize(options = nil)
|
37
|
+
@opts = options || command_line_options
|
38
|
+
@subdomain = opts[:subdomain]
|
39
|
+
@logger = opts[:logger] || Logger.new(STDOUT).tap {|l| l.level = Logger::INFO}
|
40
|
+
@conn = Faraday::Connection.new("http://#{subdomain}.zendesk.com") do |b|
|
41
|
+
b.adapter :net_http
|
42
|
+
b.use ResponseJSON
|
43
|
+
end
|
44
|
+
conn.basic_auth(opts[:email], opts[:password])
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#{self.class.name} (#{subdomain})"
|
49
|
+
end
|
50
|
+
|
51
|
+
# API helpers # {{{
|
52
|
+
def user user_id
|
53
|
+
fetch_resource("users/#{user_id}.json")
|
54
|
+
end
|
55
|
+
|
56
|
+
def users
|
57
|
+
fetch_paginated_resources("users.json?page=%d")
|
58
|
+
end
|
59
|
+
|
60
|
+
def forums
|
61
|
+
fetch_resource("forums.json")
|
62
|
+
end
|
63
|
+
|
64
|
+
def entries forum_id
|
65
|
+
fetch_paginated_resources("forums/#{forum_id}/entries.json?page=%d")
|
66
|
+
end
|
67
|
+
|
68
|
+
def posts entry_id
|
69
|
+
fetch_paginated_resources("entries/#{entry_id}/posts.json?page=%d", 'posts')
|
70
|
+
end
|
71
|
+
|
72
|
+
def open_tickets
|
73
|
+
fetch_paginated_resources("search.json?query=type:ticket+status:open+status:pending+status:new&page=%d")
|
74
|
+
end
|
75
|
+
# }}}
|
76
|
+
|
77
|
+
protected # {{{
|
78
|
+
|
79
|
+
# Fetch every page of a given resource. Must provide a sprintf format string
|
80
|
+
# with a single integer for the page specification.
|
81
|
+
#
|
82
|
+
# Example: "users.json?page=%d"
|
83
|
+
#
|
84
|
+
# In some cases the desired data is not in the top level of the payload. In
|
85
|
+
# that case specify resource_key to pull the data from that key.
|
86
|
+
def fetch_resource resource_url, resource_key = nil
|
87
|
+
debug "fetching #{resource_url}"
|
88
|
+
loop do
|
89
|
+
response = conn.get(resource_url)
|
90
|
+
if response.success?
|
91
|
+
return resource_key ? response.body[resource_key] : response.body
|
92
|
+
elsif response.status == 503
|
93
|
+
log "got a 503 (API throttle), waiting 30 seconds..."
|
94
|
+
sleep 30
|
95
|
+
else
|
96
|
+
raise Error, "failed to get resource #{resource_format}: #{response.inspect}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Fetch every page of a given resource. Must provide a sprintf format string
|
102
|
+
# with a single integer for the page specification.
|
103
|
+
#
|
104
|
+
# Example: "users.json?page=%d"
|
105
|
+
#
|
106
|
+
# In some cases the desired data is not in the top level of the payload. In
|
107
|
+
# that case specify resource_key to pull the data from that key.
|
108
|
+
def fetch_paginated_resources resource_format, resource_key = nil
|
109
|
+
resources = []
|
110
|
+
page = 1
|
111
|
+
loop do
|
112
|
+
resource = fetch_resource(resource_format % page, resource_key)
|
113
|
+
break if resource.empty?
|
114
|
+
page += 1
|
115
|
+
resources += resource
|
116
|
+
end
|
117
|
+
resources
|
118
|
+
end
|
119
|
+
|
120
|
+
def command_line_options
|
121
|
+
options = Trollop::options do
|
122
|
+
banner <<-EOM
|
123
|
+
Usage:
|
124
|
+
#{$0} -e <email> -p <password> -s <subdomain>
|
125
|
+
|
126
|
+
Prerequisites:
|
127
|
+
# Ruby gems
|
128
|
+
gem install faraday -v "~>0.4.5"
|
129
|
+
gem install trollop
|
130
|
+
gem install yajl-ruby
|
131
|
+
# Python tools (must be in your PATH)
|
132
|
+
html2text.py: http://www.aaronsw.com/2002/html2text/
|
133
|
+
|
134
|
+
Options:
|
135
|
+
EOM
|
136
|
+
opt :email, "user email address", :type => String
|
137
|
+
opt :password, "user password", :type => String
|
138
|
+
opt :subdomain, "subdomain", :type => String
|
139
|
+
end
|
140
|
+
|
141
|
+
[:email, :password, :subdomain ].each do |option|
|
142
|
+
Trollop::die option, "is required" if options[option].nil?
|
143
|
+
end
|
144
|
+
return options
|
145
|
+
end
|
146
|
+
# }}}
|
147
|
+
|
148
|
+
end # }}}
|
149
|
+
|
150
|
+
class Exporter # {{{
|
151
|
+
attr_reader :logger, :client
|
152
|
+
include Log
|
153
|
+
include FileUtils
|
154
|
+
|
155
|
+
def initialize client
|
156
|
+
@client = client
|
157
|
+
@author_email = {}
|
158
|
+
@logger = client.logger
|
159
|
+
@archive = TenderImport::Archive.new(client.subdomain)
|
160
|
+
if `which html2text.py`.empty?
|
161
|
+
raise Error, 'missing prerequisite: html2text.py is not in your PATH'
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_s
|
166
|
+
"#{self.class.name} (#{client.subdomain})"
|
167
|
+
end
|
168
|
+
|
169
|
+
def stats
|
170
|
+
@archive.stats
|
171
|
+
end
|
172
|
+
|
173
|
+
def report
|
174
|
+
@archive.report
|
175
|
+
end
|
176
|
+
|
177
|
+
def export_users # {{{
|
178
|
+
log 'exporting users'
|
179
|
+
client.users.each do |user|
|
180
|
+
@author_email[user['id'].to_s] = user['email']
|
181
|
+
log "exporting user #{user['email']}"
|
182
|
+
@archive.add_user \
|
183
|
+
:name => user['name'],
|
184
|
+
:email => user['email'],
|
185
|
+
:created_at => user['created_at'],
|
186
|
+
:updated_at => user['updated_at'],
|
187
|
+
:state => (user['roles'].to_i == 0 ? 'user' : 'support')
|
188
|
+
end
|
189
|
+
end # }}}
|
190
|
+
|
191
|
+
def export_categories # {{{
|
192
|
+
log 'exporting categories'
|
193
|
+
client.forums.each do |forum|
|
194
|
+
log "exporting category #{forum['name']}"
|
195
|
+
category = @archive.add_category \
|
196
|
+
:name => forum['name'],
|
197
|
+
:summary => forum['description']
|
198
|
+
export_discussions(forum['id'], category)
|
199
|
+
end
|
200
|
+
end # }}}
|
201
|
+
|
202
|
+
def export_tickets # {{{
|
203
|
+
log "exporting open tickets"
|
204
|
+
tickets = client.open_tickets
|
205
|
+
if tickets.size > 0
|
206
|
+
# create category for tickets
|
207
|
+
log "creating ticket category"
|
208
|
+
category = @archive.add_category \
|
209
|
+
:name => 'Tickets',
|
210
|
+
:summary => 'Imported from ZenDesk.'
|
211
|
+
# export tickets into new category
|
212
|
+
tickets.each do |ticket|
|
213
|
+
comments = ticket['comments'].map do |post|
|
214
|
+
{
|
215
|
+
:body => post['value'],
|
216
|
+
:author_email => author_email(post['author_id']),
|
217
|
+
:created_at => post['created_at'],
|
218
|
+
:updated_at => post['updated_at'],
|
219
|
+
}
|
220
|
+
end
|
221
|
+
log "exporting ticket #{ticket['nice_id']}"
|
222
|
+
@archive.add_discussion category,
|
223
|
+
:title => ticket['subject'],
|
224
|
+
:author_email => author_email(ticket['submitter_id']),
|
225
|
+
:created_at => ticket['created_at'],
|
226
|
+
:updated_at => ticket['updated_at'],
|
227
|
+
:comments => comments
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end # }}}
|
231
|
+
|
232
|
+
def export_discussions forum_id, category # {{{
|
233
|
+
client.entries(forum_id).each do |entry|
|
234
|
+
comments = client.posts(entry['id']).map do |post|
|
235
|
+
dump_body post, post['body']
|
236
|
+
{
|
237
|
+
:body => load_body(entry),
|
238
|
+
:author_email => author_email(post['user_id']),
|
239
|
+
:created_at => post['created_at'],
|
240
|
+
:updated_at => post['updated_at'],
|
241
|
+
}
|
242
|
+
end
|
243
|
+
dump_body entry, entry['body']
|
244
|
+
log "exporting discussion #{entry['title']}"
|
245
|
+
@archive.add_discussion category,
|
246
|
+
:title => entry['title'],
|
247
|
+
:author_email => author_email(entry['submitter_id']),
|
248
|
+
:comments => [{
|
249
|
+
:body => load_body(entry),
|
250
|
+
:author_email => author_email(entry['submitter_id']),
|
251
|
+
:created_at => entry['created_at'],
|
252
|
+
:updated_at => entry['updated_at'],
|
253
|
+
}] + comments
|
254
|
+
rm "tmp/#{entry['id']}_body.html"
|
255
|
+
end
|
256
|
+
end # }}}
|
257
|
+
|
258
|
+
def create_archive # {{{
|
259
|
+
export_file = @archive.write_archive
|
260
|
+
log "created #{export_file}"
|
261
|
+
end # }}}
|
262
|
+
|
263
|
+
protected
|
264
|
+
|
265
|
+
def author_email user_id
|
266
|
+
# the cache should be populated during export_users but we'll attempt
|
267
|
+
# to fetch unrecognized ids just in case
|
268
|
+
@author_email[user_id.to_s] ||= (client.user(user_id)['email'] rescue nil)
|
269
|
+
end
|
270
|
+
|
271
|
+
def dump_body entry, body
|
272
|
+
File.open(File.join("tmp", "#{entry['id']}_body.html"), "w") do |file|
|
273
|
+
file.write(body)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def load_body entry
|
278
|
+
`html2text.py /$PWD/tmp/#{entry['id']}_body.html`
|
279
|
+
end
|
280
|
+
|
281
|
+
end # }}}
|
282
|
+
|
283
|
+
# Produce a complete import archive either from API or command line options.
|
284
|
+
def self.run options=nil
|
285
|
+
begin
|
286
|
+
client = Client.new options
|
287
|
+
exporter = Exporter.new client
|
288
|
+
exporter.export_users
|
289
|
+
exporter.export_categories
|
290
|
+
exporter.export_tickets
|
291
|
+
exporter.create_archive
|
292
|
+
rescue Error => e
|
293
|
+
puts "FAILED WITH AN ERROR"
|
294
|
+
puts e.to_s
|
295
|
+
exit 1
|
296
|
+
ensure
|
297
|
+
if exporter
|
298
|
+
puts "RESULTS"
|
299
|
+
puts exporter.stats.inspect
|
300
|
+
puts exporter.report.join("\n")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
end
|
306
|
+
|
307
|
+
# vi:foldmethod=marker
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tender_import
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Zack Hobson
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-08-03 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: faraday
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: trollop
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: yajl-ruby
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
description: |
|
64
|
+
These are tools written in Ruby to support importing data into Tender.
|
65
|
+
|
66
|
+
For more information:
|
67
|
+
|
68
|
+
https://help.tenderapp.com/kb/setup-installation/importing
|
69
|
+
|
70
|
+
email: zack@zackhobson.com
|
71
|
+
executables:
|
72
|
+
- zendesk2tender
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files: []
|
76
|
+
|
77
|
+
files:
|
78
|
+
- README.md
|
79
|
+
- lib/tender_import/archive.rb
|
80
|
+
- lib/tender_import/version.rb
|
81
|
+
- lib/tender_import/zendesk_api_import.rb
|
82
|
+
- lib/tender_import.rb
|
83
|
+
- bin/zendesk2tender
|
84
|
+
has_rdoc: true
|
85
|
+
homepage: https://help.tenderapp.com/kb/setup-installation/importing
|
86
|
+
licenses: []
|
87
|
+
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options: []
|
90
|
+
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
hash: 3
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
version: "0"
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
hash: 3
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
requirements: []
|
112
|
+
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.4.1
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: Tools for producing Tender import archives.
|
118
|
+
test_files: []
|
119
|
+
|