tender_import 0.0.1
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/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
|
+
|