zensana 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|