zensana 1.0.0
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +55 -0
- data/LICENCE.txt +22 -0
- data/README.md +165 -0
- data/Rakefile +2 -0
- data/bin/zensana +7 -0
- data/lib/zensana/cli.rb +8 -0
- data/lib/zensana/command.rb +7 -0
- data/lib/zensana/commands/project.rb +293 -0
- data/lib/zensana/models/asana/attachment.rb +62 -0
- data/lib/zensana/models/asana/project.rb +84 -0
- data/lib/zensana/models/asana/task.rb +86 -0
- data/lib/zensana/models/asana/user.rb +35 -0
- data/lib/zensana/models/zendesk/attachment.rb +38 -0
- data/lib/zensana/models/zendesk/comment.rb +34 -0
- data/lib/zensana/models/zendesk/ticket.rb +73 -0
- data/lib/zensana/models/zendesk/user.rb +96 -0
- data/lib/zensana/services/asana.rb +48 -0
- data/lib/zensana/services/error.rb +55 -0
- data/lib/zensana/services/response.rb +49 -0
- data/lib/zensana/services/zendesk.rb +54 -0
- data/lib/zensana/validate/key.rb +44 -0
- data/lib/zensana/version.rb +3 -0
- data/lib/zensana.rb +25 -0
- data/zensana.gemspec +31 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea308a2a19d3adbe4fbd80bca3ddc3a09042668b
|
4
|
+
data.tar.gz: fa62e437c65bd88ebf8a2ec4ff0d716ae04813f8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d0383bcc73118353f5c0bacf11beb5fd10ce72bd6b663ceafddf5ca521b5a6387d80aec6b221a6646f5104a93ec1bc51fe9e7db65428b71def38b6c11a8132a2
|
7
|
+
data.tar.gz: 1b81528f9917e9f6982e81355c0fa595711625c26fe286f1ec209920b9b0c5ffef6772d5cd9e890ca8d836e60cbe30e83f0d85939e66e5732175103f76140ebb
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
5
|
+
# rspec may be run, below are examples of the most common uses.
|
6
|
+
# * bundler: 'bundle exec rspec'
|
7
|
+
# * bundler binstubs: 'bin/rspec'
|
8
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
9
|
+
# installed the spring binstubs per the docs)
|
10
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
11
|
+
# * 'just' rspec: 'rspec'
|
12
|
+
|
13
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
14
|
+
require "guard/rspec/dsl"
|
15
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
16
|
+
|
17
|
+
# Feel free to open issues for suggestions and improvements
|
18
|
+
|
19
|
+
# RSpec files
|
20
|
+
rspec = dsl.rspec
|
21
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
22
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
23
|
+
watch(rspec.spec_files)
|
24
|
+
|
25
|
+
# Ruby files
|
26
|
+
ruby = dsl.ruby
|
27
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
28
|
+
|
29
|
+
# Rails files
|
30
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
31
|
+
dsl.watch_spec_files_for(rails.app_files)
|
32
|
+
dsl.watch_spec_files_for(rails.views)
|
33
|
+
|
34
|
+
watch(rails.controllers) do |m|
|
35
|
+
[
|
36
|
+
rspec.spec.("routing/#{m[1]}_routing"),
|
37
|
+
rspec.spec.("controllers/#{m[1]}_controller"),
|
38
|
+
rspec.spec.("acceptance/#{m[1]}")
|
39
|
+
]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Rails config changes
|
43
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
44
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
45
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
46
|
+
|
47
|
+
# Capybara features specs
|
48
|
+
watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
|
49
|
+
|
50
|
+
# Turnip features and steps
|
51
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
52
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
53
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
54
|
+
end
|
55
|
+
end
|
data/LICENCE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Warren Bain
|
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,165 @@
|
|
1
|
+
# Zensana
|
2
|
+
|
3
|
+
This gem provides access to the Asana API and ZenDesk Ticket Import API
|
4
|
+
for the purpose of importing tasks from Asana Projects into ZenDesk
|
5
|
+
tickets.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'zensana'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install zensana
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
I had a specific use-case in developing this: to get off Asana as a
|
26
|
+
support ticketing system and onto ZenDesk. So there aren't many tests
|
27
|
+
(I know, I know) and there are only a few commands available in the cli.
|
28
|
+
|
29
|
+
### CLI
|
30
|
+
|
31
|
+
This uses Thor so follows the general pattern of
|
32
|
+
|
33
|
+
$ zensana COMMAND SUBCOMMAND OPTIONS
|
34
|
+
|
35
|
+
The help is pretty self-explanatory around the options so just try that
|
36
|
+
|
37
|
+
$ zensana help
|
38
|
+
Commands:
|
39
|
+
zensana help [COMMAND] # Describe available commands or one specific command
|
40
|
+
zensana project SUBCOMMAND # perform actions on Asana projects
|
41
|
+
|
42
|
+
#### project command
|
43
|
+
|
44
|
+
The primary use for zensana is to convert an Asana project's tasks into
|
45
|
+
ZenDesk tickets. The `convert` subcommand has quite a few options to
|
46
|
+
control what gets converted.
|
47
|
+
|
48
|
+
$ zensana project help convert
|
49
|
+
Usage:
|
50
|
+
zensana convert PROJECT
|
51
|
+
|
52
|
+
Options:
|
53
|
+
-a, [--attachments], [--no-attachments] # download and upload any attachments
|
54
|
+
# Default: true
|
55
|
+
-c, [--completed], [--no-completed] # include tasks that are completed
|
56
|
+
-u, [--default-user=DEFAULT_USER] # set a default user to assign to tickets
|
57
|
+
-s, [--stories], [--no-stories] # import stories as comments
|
58
|
+
# Default: true
|
59
|
+
-v, [--verified], [--no-verified] # `false` will send email to zendesk users created
|
60
|
+
# Default: true
|
61
|
+
|
62
|
+
Convert PROJECT tasks to ZenDesk tickets (exact ID or NAME required)
|
63
|
+
|
64
|
+
#### idempotentcy
|
65
|
+
|
66
|
+
To ensure robustness of the conversion, especially given that internet
|
67
|
+
connection issues may interrupt it, the project conversion is idempotent
|
68
|
+
and can be restarted as many times as necessary. In particular
|
69
|
+
|
70
|
+
* tasks that have already been successfully created will not be
|
71
|
+
recreated as tickets (the Asana task_id is stored as the Zendesk
|
72
|
+
ticket external_id)
|
73
|
+
* attachments will only be downloaded if they do not exist in the
|
74
|
+
download directory
|
75
|
+
|
76
|
+
### Classes
|
77
|
+
|
78
|
+
You can also directly access classes which model Asana data objects.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
require 'zensana'
|
82
|
+
|
83
|
+
my_project = Zensana::Asana::Project.new('My awesome Asana Project')
|
84
|
+
|
85
|
+
my_project.full_tasks.each do |task|
|
86
|
+
puts "Task #{task.name} has tags: #{task.tags}"
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Check ../commands/project.rb for examples in action.
|
91
|
+
|
92
|
+
#### Asana
|
93
|
+
|
94
|
+
There is support for reading:
|
95
|
+
* user
|
96
|
+
* project
|
97
|
+
* task
|
98
|
+
* attachment - downloading
|
99
|
+
|
100
|
+
#### ZenDesk
|
101
|
+
|
102
|
+
There is support for accessing and updating:
|
103
|
+
* user
|
104
|
+
* ticket - via the Ticket Import API only
|
105
|
+
* comment
|
106
|
+
* attachment - uploading
|
107
|
+
|
108
|
+
## Authenticating
|
109
|
+
|
110
|
+
### Asana
|
111
|
+
|
112
|
+
Asana users can connect to the API using their username / password or
|
113
|
+
by creating an API key. The following environment vars are required:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
ASANA_API_KEY
|
117
|
+
or
|
118
|
+
ASANA_USERNAME
|
119
|
+
ASANA_PASSWORD
|
120
|
+
```
|
121
|
+
|
122
|
+
### ZenDesk
|
123
|
+
|
124
|
+
ZenDesk by default requires username / password to connect to the API,
|
125
|
+
and the endpoint is defined by the organisation domain name. The following
|
126
|
+
environment vars are required:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
ZENDESK_USERNAME
|
130
|
+
ZENDESK_PASSWORD
|
131
|
+
ZENDESK_DOMAIN
|
132
|
+
```
|
133
|
+
|
134
|
+
## Contributing
|
135
|
+
|
136
|
+
1. Fork it ( https://github.com/thoughtcroft/zensana/fork )
|
137
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
138
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
139
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
140
|
+
5. Create a new Pull Request
|
141
|
+
|
142
|
+
# License
|
143
|
+
|
144
|
+
### This code is free to use under the terms of the MIT license.
|
145
|
+
|
146
|
+
Copyright (c) 2015 Warren Bain
|
147
|
+
|
148
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
149
|
+
a copy of this software and associated documentation files (the
|
150
|
+
"Software"), to deal in the Software without restriction, including
|
151
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
152
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
153
|
+
permit persons to whom the Software is furnished to do so, subject to
|
154
|
+
the following conditions:
|
155
|
+
|
156
|
+
The above copyright notice and this permission notice shall be
|
157
|
+
included in all copies or substantial portions of the Software.
|
158
|
+
|
159
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
160
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
161
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
162
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
163
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
164
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
165
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/bin/zensana
ADDED
data/lib/zensana/cli.rb
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Command::Project < Zensana::Command
|
3
|
+
|
4
|
+
desc 'find PROJECT', 'List projects that match PROJECT (by ID or NAME, regexp accepted)'
|
5
|
+
def find(name)
|
6
|
+
puts Zensana::Asana::Project.search(name).collect { |p| p['name'] }.sort
|
7
|
+
end
|
8
|
+
|
9
|
+
desc 'show PROJECT', 'Display details of PROJECT (choosing from list matching ID or NAME, regexp accepted)'
|
10
|
+
option :tasks, type: 'boolean', aliases: '-t', default: true, desc: 'display project task summary'
|
11
|
+
def show(project)
|
12
|
+
candidates = Zensana::Asana::Project.search(project)
|
13
|
+
|
14
|
+
if candidates.empty?
|
15
|
+
say "\nNo project found matching '#{project}'", :red
|
16
|
+
else
|
17
|
+
result = select_project(candidates)
|
18
|
+
candidate = Zensana::Asana::Project.new(result)
|
19
|
+
say "\nProject attributes...", :green
|
20
|
+
puts candidate.attributes.pretty
|
21
|
+
|
22
|
+
if options[:tasks]
|
23
|
+
say "Associated tasks...", :green
|
24
|
+
puts candidate.task_list.pretty
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'convert PROJECT', 'Convert PROJECT tasks to ZenDesk tickets (exact ID or NAME required)'
|
30
|
+
option :attachments, type: 'boolean', aliases: '-a', default: true, desc: 'download and upload any attachments'
|
31
|
+
option :completed, type: 'boolean', aliases: '-c', default: false, desc: 'include tasks that are completed'
|
32
|
+
option :default_user, type: 'string', aliases: '-u', default: nil, desc: 'set a default user to assign to tickets'
|
33
|
+
# option :followers, type: 'boolean', aliases: '-f', default: false, desc: 'add followers of tasks to tickets'
|
34
|
+
option :stories, type: 'boolean', aliases: '-s', default: true, desc: 'import stories as comments'
|
35
|
+
option :verified, type: 'boolean', aliases: '-v', default: true, desc: '`false` will send email to zendesk users created'
|
36
|
+
def convert(project)
|
37
|
+
@asana_project = Zensana::Asana::Project.new(project)
|
38
|
+
say <<-BANNER
|
39
|
+
|
40
|
+
This will convert the following Asana project into ZenDesk:
|
41
|
+
|
42
|
+
id: #{@asana_project.id}
|
43
|
+
name: #{@asana_project.name}
|
44
|
+
|
45
|
+
using options #{options}
|
46
|
+
|
47
|
+
BANNER
|
48
|
+
|
49
|
+
unless yes?("Do you wish to proceed?", :yellow)
|
50
|
+
say "\nNothing else for me to do, exiting...\n", :red
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
|
54
|
+
# LIFO tags for recursive tagging
|
55
|
+
#
|
56
|
+
# `project_tag_list` holds the project tags
|
57
|
+
# and also the tags for the current task and
|
58
|
+
# its parent tasks which are all added to the ticket
|
59
|
+
#
|
60
|
+
# `section_tag_list` holds the tags for the last
|
61
|
+
# section task which are also added to tickets
|
62
|
+
#
|
63
|
+
tags = [ 'zensana', 'imported' ]
|
64
|
+
project_tags = [] << tags
|
65
|
+
section_tags = []
|
66
|
+
|
67
|
+
@asana_project.full_tasks.each do |task|
|
68
|
+
task_to_ticket task, project_tags, section_tags
|
69
|
+
end
|
70
|
+
say "\n\n ---> Finished!\n\n", :green
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# display list of projects by name
|
76
|
+
# and prompt for selection by index
|
77
|
+
#
|
78
|
+
# returns selected project_id
|
79
|
+
#
|
80
|
+
def select_project(array)
|
81
|
+
return array.first['id'] if array.size == 1
|
82
|
+
|
83
|
+
str_format = "\n %#{array.count.to_s.size}s: %s"
|
84
|
+
question = set_color "\nWhich project should I use?", :yellow
|
85
|
+
answers = {}
|
86
|
+
|
87
|
+
array.sort_by { |e| e['name'] }.each_with_index do |project, index|
|
88
|
+
i = (index + 1).to_s
|
89
|
+
answers[i] = project['id']
|
90
|
+
question << format(str_format, i, project['name'])
|
91
|
+
end
|
92
|
+
|
93
|
+
puts question
|
94
|
+
reply = ask("> ").to_s
|
95
|
+
if answers[reply]
|
96
|
+
answers[reply]
|
97
|
+
else
|
98
|
+
say "Not a valid selection, I'm out of here!", :red
|
99
|
+
exit 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# convert and asana task into a zendesk ticket
|
104
|
+
# calls itself recursively for subtasks
|
105
|
+
#
|
106
|
+
def task_to_ticket(task, project_tags, section_tags )
|
107
|
+
if task.attributes['completed'] && !options[:completed]
|
108
|
+
say "\nSkipping completed task: #{task.name}! ", :yellow
|
109
|
+
return
|
110
|
+
end
|
111
|
+
|
112
|
+
# sections contribute their tags but nothing else
|
113
|
+
# and the same is true of tasks already created
|
114
|
+
if task.is_section?
|
115
|
+
say "\nProcessing section: #{task.section_name} "
|
116
|
+
section_tags.pop
|
117
|
+
section_tags.push task.tags << snake_case(task.section_name)
|
118
|
+
else
|
119
|
+
say "\nProcessing task: #{task.name} "
|
120
|
+
project_tags.push snake_case(task.tags)
|
121
|
+
|
122
|
+
if Zensana::Zendesk::Ticket.external_id_exists?(task.id)
|
123
|
+
say "\n >>> skip ticket creation, task already imported ", :yellow
|
124
|
+
else
|
125
|
+
requester = asana_to_zendesk_user(task.created_by, true)
|
126
|
+
|
127
|
+
# create comments from the task's stories
|
128
|
+
if options[:stories]
|
129
|
+
|
130
|
+
comments = []
|
131
|
+
task.stories.each do |story|
|
132
|
+
if story['type'] == 'comment' &&
|
133
|
+
(author = asana_to_zendesk_user(story['created_by'], true))
|
134
|
+
comments << Zensana::Zendesk::Comment.new(
|
135
|
+
:author_id => author.id,
|
136
|
+
:value => story['text'],
|
137
|
+
:created_at => story['created_at'],
|
138
|
+
:public => true
|
139
|
+
).attributes
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# process attachments on this task
|
144
|
+
if options[:attachments]
|
145
|
+
download_attachments task.attachments
|
146
|
+
if tokens = upload_attachments(task.attachments)
|
147
|
+
comments << Zensana::Zendesk::Comment.new(
|
148
|
+
:author_id => requester.id,
|
149
|
+
:value => 'Attachments from original Asana task',
|
150
|
+
:uploads => tokens,
|
151
|
+
:public => true
|
152
|
+
).attributes
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# if assignee is not an agent then leave unassigned
|
158
|
+
if (assignee_key = options[:default_user] || task.attributes['assignee'])
|
159
|
+
unless (assignee = asana_to_zendesk_user(assignee_key, false)) &&
|
160
|
+
(assignee.role != 'end-user')
|
161
|
+
assignee = nil
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# ready to import the ticket now!
|
166
|
+
ticket = Zensana::Zendesk::Ticket.new(
|
167
|
+
:requester_id => requester.id,
|
168
|
+
:external_id => task.id,
|
169
|
+
:subject => task.name,
|
170
|
+
:description => <<-EOF,
|
171
|
+
This is an Asana task imported using zensana @ #{Time.now}
|
172
|
+
|
173
|
+
Project: #{@asana_project.name} (#{@asana_project.id})
|
174
|
+
|
175
|
+
Task notes: #{task.notes}
|
176
|
+
|
177
|
+
Task attributes: #{task.attributes}
|
178
|
+
EOF
|
179
|
+
:assignee_id => assignee ? assignee.id : '',
|
180
|
+
:created_at => task.created_at,
|
181
|
+
:tags => flatten_tags(project_tags, section_tags),
|
182
|
+
:comments => comments
|
183
|
+
)
|
184
|
+
ticket.import
|
185
|
+
end
|
186
|
+
|
187
|
+
# rinse and repeat for subtasks and their subtasks and ...
|
188
|
+
# we create a new section tag list for each recursed level
|
189
|
+
sub_section_tags = []
|
190
|
+
task.subtasks.each do |sub|
|
191
|
+
task_to_ticket Zensana::Asana::Task.new(sub.attributes['id']), project_tags, sub_section_tags
|
192
|
+
end
|
193
|
+
|
194
|
+
# this task's tags are now no longer required
|
195
|
+
project_tags.pop
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# lookup up asana user on zendesk and
|
200
|
+
# optionally create new if not exists
|
201
|
+
#
|
202
|
+
# return: zendesk user or nil
|
203
|
+
#
|
204
|
+
def asana_to_zendesk_user(spec, create)
|
205
|
+
key = spec.is_a?(Hash) ? spec['id'] : spec
|
206
|
+
asana = Zensana::Asana::User.new(key)
|
207
|
+
zendesk = Zensana::Zendesk::User.new
|
208
|
+
zendesk.find(asana.email)
|
209
|
+
rescue NotFound
|
210
|
+
if create
|
211
|
+
zendesk.create(
|
212
|
+
:email => asana.email,
|
213
|
+
:name => asana.name,
|
214
|
+
:verified => options[:verified]
|
215
|
+
)
|
216
|
+
zendesk
|
217
|
+
else
|
218
|
+
nil
|
219
|
+
end
|
220
|
+
else
|
221
|
+
zendesk
|
222
|
+
end
|
223
|
+
|
224
|
+
# download the attachments to the local file system
|
225
|
+
# and retry a few times because internet
|
226
|
+
# the download process is restartable (idempotent)
|
227
|
+
#
|
228
|
+
def download_attachments(attachments)
|
229
|
+
return if attachments.nil? || attachments.empty?
|
230
|
+
say "\n >>> downloading attachments "
|
231
|
+
|
232
|
+
attachments.each do |attachment|
|
233
|
+
tries = 3
|
234
|
+
begin
|
235
|
+
attachment.download
|
236
|
+
print '.'
|
237
|
+
rescue
|
238
|
+
retry unless (tries-= 1).zero?
|
239
|
+
raise
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# upload all of the attachments from the local file system
|
245
|
+
# and retry a few times because internet
|
246
|
+
#
|
247
|
+
# return: array of tokens
|
248
|
+
#
|
249
|
+
def upload_attachments(attachments)
|
250
|
+
return if attachments.nil? || attachments.empty?
|
251
|
+
say "\n >>> uploading attachments "
|
252
|
+
|
253
|
+
[].tap do |tokens|
|
254
|
+
attachments.each do |attachment|
|
255
|
+
tries = 3
|
256
|
+
begin
|
257
|
+
uploader = Zensana::Zendesk::Attachment.new
|
258
|
+
tokens << uploader.upload(attachment.full_path)['token']
|
259
|
+
print '.'
|
260
|
+
rescue
|
261
|
+
retry unless (tries-= 1).zero?
|
262
|
+
raise
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# take multiple arrays and flatten them
|
269
|
+
#
|
270
|
+
# return: single array of unique tags
|
271
|
+
#
|
272
|
+
def flatten_tags(*args)
|
273
|
+
tags = []
|
274
|
+
args.each { |a| tags << a }
|
275
|
+
tags.flatten.uniq
|
276
|
+
end
|
277
|
+
|
278
|
+
def snake_case(thing)
|
279
|
+
case
|
280
|
+
when thing.is_a?(String)
|
281
|
+
snake_case_it thing
|
282
|
+
when thing.is_a?(Array)
|
283
|
+
thing.map { |a| snake_case a }
|
284
|
+
else
|
285
|
+
raise ArgumentError, "I don't know how to snake_case instances of #{thing.class}"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def snake_case_it(thing)
|
290
|
+
thing.gsub(/(.)([A-Z])/,'\1_\2').gsub(' ', '_').downcase
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Asana
|
3
|
+
class Attachment
|
4
|
+
include Zensana::Asana::Access
|
5
|
+
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
def initialize(id)
|
9
|
+
@attributes = fetch(id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def download
|
13
|
+
download_file unless downloaded?
|
14
|
+
end
|
15
|
+
|
16
|
+
def download!
|
17
|
+
File.delete full_path if downloaded?
|
18
|
+
download_file
|
19
|
+
end
|
20
|
+
|
21
|
+
def downloaded?
|
22
|
+
File.exist? full_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_path
|
26
|
+
File.join(file_dir, attributes['name'])
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(name, *args, &block)
|
30
|
+
attributes[name.to_s] || super
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def file_dir
|
36
|
+
File.join(temp_dir, 'downloads', parent)
|
37
|
+
end
|
38
|
+
|
39
|
+
def temp_dir
|
40
|
+
ENV['ZENSANA_TEMP_DIR'] || '/tmp/zensana'
|
41
|
+
end
|
42
|
+
|
43
|
+
def parent
|
44
|
+
attributes['parent']['id'].to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch(id)
|
48
|
+
asana_service.fetch("/attachments/#{id}")
|
49
|
+
end
|
50
|
+
|
51
|
+
def download_file
|
52
|
+
FileUtils.mkdir_p file_dir
|
53
|
+
result = File.open(full_path, "wb") do |f|
|
54
|
+
f.write HTTParty.get(self.download_url)
|
55
|
+
end
|
56
|
+
|
57
|
+
Zensana::Error.handle_https result
|
58
|
+
Zensana::Response.new(result).ok?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|