lionel_richie 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +49 -0
- data/bin/lionel +32 -98
- data/lib/lionel.rb +18 -0
- data/lib/lionel/cli.rb +65 -0
- data/lib/lionel/configurable.rb +33 -0
- data/lib/lionel/configuration.rb +79 -0
- data/lib/lionel/export.rb +162 -0
- data/lib/lionel/google_authentication.rb +58 -0
- data/lib/lionel/proxy_action.rb +48 -0
- data/lib/lionel/proxy_card.rb +62 -0
- data/lib/lionel/proxy_worksheet.rb +25 -0
- data/lib/lionel/trello_authentication.rb +34 -0
- data/lib/lionel/version.rb +3 -0
- data/lib/lionel_richie.rb +1 -8
- data/lionel_richie.gemspec +6 -3
- data/script/console +13 -0
- metadata +20 -11
- data/bin/authorize_lionel +0 -61
- data/lib/lionel_richie/version.rb +0 -3
data/README.md
CHANGED
@@ -49,6 +49,55 @@ You should now be ready to run the export:
|
|
49
49
|
|
50
50
|
$ lionel
|
51
51
|
|
52
|
+
## Crafting the Export
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
|
56
|
+
LionelRichie.export do
|
57
|
+
# Card Id
|
58
|
+
b { id }
|
59
|
+
|
60
|
+
# Card Link
|
61
|
+
c { link }
|
62
|
+
|
63
|
+
# Ready date
|
64
|
+
d do
|
65
|
+
ready_action = card.first_action do |a|
|
66
|
+
(a.create? && a.board_id == trello_board_id) || a.moved_to?("Ready")
|
67
|
+
end
|
68
|
+
format_date(ready_action.date) if ready_action
|
69
|
+
end
|
70
|
+
|
71
|
+
# In Progress date
|
72
|
+
e { date_moved_to("In Progress") }
|
73
|
+
|
74
|
+
# Code Review date
|
75
|
+
f { date_moved_to("Code Review") }
|
76
|
+
|
77
|
+
# Review date
|
78
|
+
g { date_moved_to("Review") }
|
79
|
+
|
80
|
+
# Deploy date
|
81
|
+
h { date_moved_to("Deploy") }
|
82
|
+
|
83
|
+
# Completed date
|
84
|
+
i { date_moved_to("Completed") }
|
85
|
+
|
86
|
+
# Type
|
87
|
+
j { type }
|
88
|
+
|
89
|
+
# Project
|
90
|
+
k { project }
|
91
|
+
|
92
|
+
# Estimate
|
93
|
+
l { estimate }
|
94
|
+
|
95
|
+
# Due Date
|
96
|
+
m { due_date }
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
|
52
101
|
## Contributing
|
53
102
|
|
54
103
|
1. Fork it
|
data/bin/lionel
CHANGED
@@ -1,109 +1,43 @@
|
|
1
1
|
#! /usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
# Trap interrupts to quit cleanly. See
|
4
|
+
# https://twitter.com/mitchellh/status/283014103189053442
|
5
|
+
Signal.trap("INT") { exit 1 }
|
4
6
|
|
5
7
|
require 'lionel_richie'
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
# Output message to $stderr, prefixed with the program name
|
10
|
+
def pute(*args)
|
11
|
+
first = args.shift.dup
|
12
|
+
first.insert(0, "#{$0}: ")
|
13
|
+
args.unshift(first)
|
14
|
+
$stderr.puts(*args)
|
10
15
|
end
|
11
16
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
start_row = 2
|
28
|
-
rows = ws.rows.size
|
29
|
-
|
30
|
-
card_ids = cards.map(&:id)
|
31
|
-
|
32
|
-
def sync_row(ws, row, card)
|
33
|
-
puts "syncing row[#{row}] with #{card.name}"
|
34
|
-
ws["B#{row}"] = card.id
|
35
|
-
|
36
|
-
# Card link
|
37
|
-
ws["C#{row}"] = %Q[=HYPERLINK("#{card.url}", "#{card.name.gsub(/"/, "")}")]
|
38
|
-
|
39
|
-
actions = card.actions
|
40
|
-
actions = actions.select { |a| a.data["listBefore"].present? }
|
41
|
-
|
42
|
-
# Ready date
|
43
|
-
ws["D#{row}"] = action_date(actions) do |a|
|
44
|
-
(a.type == "createCard" && a.data["board"]["id"] == board_id) ||
|
45
|
-
a.data["listAfter"]["name"] =~ /ready/i
|
17
|
+
begin
|
18
|
+
Lionel::CLI.start(ARGV)
|
19
|
+
|
20
|
+
rescue GoogleDrive::Error, GoogleDrive::AuthenticationError => e
|
21
|
+
@attempts ||= 0
|
22
|
+
@attempts += 1
|
23
|
+
Lionel::GoogleAuthentication.new.refresh
|
24
|
+
if @attempts < 2
|
25
|
+
retry
|
26
|
+
else
|
27
|
+
puts e.class
|
28
|
+
puts "-" * e.class.name.size
|
29
|
+
pute e.message
|
30
|
+
puts "Unable to access Google Drive"
|
31
|
+
puts "run 'lionel authorize'"
|
46
32
|
end
|
47
33
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
# Code Review date
|
52
|
-
ws["F#{row}"] = action_date(actions) { |a| a.data["listAfter"]["name"] =~ /^code review/i }
|
53
|
-
|
54
|
-
# Review date
|
55
|
-
ws["G#{row}"] = action_date(actions) { |a| a.data["listAfter"]["name"] =~ /^review/i }
|
56
|
-
|
57
|
-
# Deploy date
|
58
|
-
ws["H#{row}"] = action_date(actions) { |a| a.data["listAfter"]["name"] =~ /^deploy/i }
|
34
|
+
rescue Trello::Error, Trello::InvalidAccessToken => e
|
35
|
+
puts "Unable to access Trello"
|
36
|
+
puts "run 'lionel authorize'"
|
59
37
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
type = labels.detect { |l| l.downcase =~ %r{bug|chore|task}i } || 'story'
|
66
|
-
ws["J#{row}"] = type
|
67
|
-
|
68
|
-
# Estimate
|
69
|
-
match = card.name.match(/\[(?<estimate>\w)\]/)
|
70
|
-
ws["L#{row}"] = match[:estimate] if match
|
71
|
-
|
72
|
-
rescue Trello::Error => e
|
73
|
-
puts e.inspect
|
74
|
-
puts card.inspect
|
75
|
-
end
|
76
|
-
|
77
|
-
def action_date(actions, &block)
|
78
|
-
actions = actions.select(&block)
|
79
|
-
return "" if actions.empty?
|
80
|
-
action = actions.sort { |a, b| a.date <=> b.date }.first
|
81
|
-
action.date.strftime("%m/%d/%Y")
|
82
|
-
end
|
83
|
-
|
84
|
-
card_rows = {}
|
85
|
-
|
86
|
-
# Find existing rows for current cards
|
87
|
-
(start_row..rows).each do |row|
|
88
|
-
cell_id = ws["B#{row}"]
|
89
|
-
next unless cell_id.present?
|
90
|
-
card = cards.find { |c| c.id == cell_id }
|
91
|
-
next unless card.present?
|
92
|
-
card_rows[row] = card
|
93
|
-
end
|
94
|
-
|
95
|
-
# Set available rows for new cards
|
96
|
-
new_cards = cards - card_rows.values
|
97
|
-
new_cards.each_with_index do |card, i|
|
98
|
-
row = rows + i + 1
|
99
|
-
card_rows[row] = card
|
38
|
+
rescue StandardError => e
|
39
|
+
puts e.class
|
40
|
+
puts "-" * e.class.name.size
|
41
|
+
pute e.message
|
42
|
+
raise e
|
100
43
|
end
|
101
|
-
|
102
|
-
card_rows.each do |row, card|
|
103
|
-
sleep 1
|
104
|
-
Timeout.timeout(5) { sync_row(ws, row, card) }
|
105
|
-
end
|
106
|
-
|
107
|
-
ws.save
|
108
|
-
|
109
|
-
puts "exiting"
|
data/lib/lionel.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'yajl'
|
2
|
+
require 'trello'
|
3
|
+
require 'google_drive'
|
4
|
+
require 'launchy'
|
5
|
+
require 'thor'
|
6
|
+
require 'lionel/version'
|
7
|
+
require 'lionel/cli'
|
8
|
+
require 'lionel/configuration'
|
9
|
+
require 'lionel/configurable'
|
10
|
+
require 'lionel/export'
|
11
|
+
require 'lionel/proxy_action'
|
12
|
+
require 'lionel/proxy_card'
|
13
|
+
require 'lionel/proxy_worksheet'
|
14
|
+
require 'lionel/trello_authentication'
|
15
|
+
require 'lionel/google_authentication'
|
16
|
+
|
17
|
+
module Lionel
|
18
|
+
end
|
data/lib/lionel/cli.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module Lionel
|
2
|
+
class CLI < Thor
|
3
|
+
|
4
|
+
def initialize(*)
|
5
|
+
@configuration = Lionel::Configuration.instance
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "authorize PROVIDER", "Allows application to request user authorization for provider (google|trello)"
|
10
|
+
def authorize(provider)
|
11
|
+
case provider
|
12
|
+
when 'trello'
|
13
|
+
auth = Lionel::TrelloAuthentication.new
|
14
|
+
|
15
|
+
Launchy.open(auth.trello_key_url)
|
16
|
+
auth.trello_key = ask "Enter trello key:"
|
17
|
+
|
18
|
+
Launchy.open(auth.trello_token_url)
|
19
|
+
auth.trello_token = ask "Enter trello token:"
|
20
|
+
|
21
|
+
auth.save
|
22
|
+
when 'google'
|
23
|
+
auth = Lionel::GoogleAuthentication.new
|
24
|
+
|
25
|
+
Launchy.open(auth.api_console_url)
|
26
|
+
auth.google_client_id = ask("Enter your google client id:")
|
27
|
+
auth.google_client_secret = ask("Enter your google client secret:")
|
28
|
+
|
29
|
+
Launchy.open(auth.authorize_url)
|
30
|
+
auth.retrieve_access_token ask("Enter your google key:")
|
31
|
+
|
32
|
+
auth.save
|
33
|
+
else
|
34
|
+
"Provider not recognized: #{provider}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "export", "Saves Trello export to Google Docs"
|
39
|
+
method_option "print", :aliases => "-p", :type => :boolean, :default => false, :desc => "Print results instead of saving them to Google Docs."
|
40
|
+
def export
|
41
|
+
export = Lionel::Export.new
|
42
|
+
|
43
|
+
unless export.has_sources?
|
44
|
+
export.trello_board_id = ask("Enter a trello board id to export from:")
|
45
|
+
export.google_doc_id = ask("Enter a google doc id to export to:")
|
46
|
+
export.save
|
47
|
+
end
|
48
|
+
|
49
|
+
export.authenticate
|
50
|
+
|
51
|
+
welcome = "Trello? Is it me you're looking for?"
|
52
|
+
say welcome
|
53
|
+
say '=' * welcome.size
|
54
|
+
|
55
|
+
export.download
|
56
|
+
|
57
|
+
if options['print']
|
58
|
+
export.rows.each { |row| say row }
|
59
|
+
else
|
60
|
+
export.upload
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Lionel
|
2
|
+
module Configurable
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
def configuration
|
8
|
+
Configuration.instance
|
9
|
+
end
|
10
|
+
|
11
|
+
def save
|
12
|
+
configuration.save(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def data
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
def config_accessor(*args)
|
24
|
+
attr_writer(*args)
|
25
|
+
|
26
|
+
args.each do |reader|
|
27
|
+
define_method(reader) do
|
28
|
+
instance_variable_get("@#{reader}") || configuration.send(reader)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Lionel
|
4
|
+
class Configuration
|
5
|
+
include Singleton
|
6
|
+
attr_reader :path, :data
|
7
|
+
|
8
|
+
FILE_NAME = '.lionelrc'
|
9
|
+
CONFIG_ACCESSORS = [
|
10
|
+
:trello_key, :trello_token, :trello_board_id,
|
11
|
+
:google_token, :google_refresh_token,
|
12
|
+
:google_client_id, :google_client_secret,
|
13
|
+
:google_doc_id
|
14
|
+
]
|
15
|
+
|
16
|
+
def self.config_accessor(*args)
|
17
|
+
delegate(*args, to: :data)
|
18
|
+
|
19
|
+
args.each do |accessor|
|
20
|
+
define_method("#{accessor}=") do |value|
|
21
|
+
data.send("#{accessor}=", value)
|
22
|
+
write
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
config_accessor(*CONFIG_ACCESSORS)
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@path = File.join(File.expand_path("~"), FILE_NAME)
|
31
|
+
@data = OpenStruct.new(load_data)
|
32
|
+
end
|
33
|
+
|
34
|
+
def save(attrs = {})
|
35
|
+
attrs.each do |accessor, value|
|
36
|
+
data.send("#{accessor}=", value)
|
37
|
+
end
|
38
|
+
write
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_data
|
42
|
+
load_file
|
43
|
+
rescue Errno::ENOENT
|
44
|
+
puts "Couldn't load file, falling back to ENV"
|
45
|
+
default_data
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_data
|
49
|
+
# {
|
50
|
+
# 'trello_key' => ENV['TRELLO_KEY'],
|
51
|
+
# 'trello_token' => ENV['TRELLO_TOKEN'],
|
52
|
+
# 'trello_board_id' => ENV['TRELLO_BOARD_ID'],
|
53
|
+
# 'google_token' => ENV['GOOGLE_TOKEN'],
|
54
|
+
# 'google_refresh_token' => ENV['GOOGLE_REFRESH_TOKEN'],
|
55
|
+
# 'google_doc_id' => ENV['GOOGLE_DOC_ID']
|
56
|
+
# 'google_client_id' => ENV['GOOGLE_CLIENT_ID']
|
57
|
+
# 'google_client_secret' => ENV['GOOGLE_CLIENT_SECRET']
|
58
|
+
# }
|
59
|
+
{}.tap do |data|
|
60
|
+
CONFIG_ACCESSORS.each do |name|
|
61
|
+
data[name] = ENV[name.to_s.upcase]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_file
|
67
|
+
require 'yaml'
|
68
|
+
YAML.load_file(@path)
|
69
|
+
end
|
70
|
+
|
71
|
+
def write
|
72
|
+
require 'yaml'
|
73
|
+
File.open(@path, File::RDWR|File::TRUNC|File::CREAT, 0600) do |rcfile|
|
74
|
+
rcfile.write @data.marshal_dump.to_yaml
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module Lionel
|
2
|
+
class Export
|
3
|
+
include Configurable
|
4
|
+
|
5
|
+
config_accessor :google_doc_id, :trello_board_id
|
6
|
+
|
7
|
+
def has_sources?
|
8
|
+
trello_board_id && google_doc_id
|
9
|
+
end
|
10
|
+
|
11
|
+
def data
|
12
|
+
{
|
13
|
+
trello_board_id: trello_board_id,
|
14
|
+
google_doc_id: google_doc_id
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def board
|
19
|
+
@board ||= Trello::Board.find(trello_board_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def cards
|
23
|
+
cards ||= [].tap do |c|
|
24
|
+
# iterate over active lists rather
|
25
|
+
# than retrieving all historical cards;
|
26
|
+
# trello api returns association proxy
|
27
|
+
# that does not respond to "flatten"
|
28
|
+
board.lists.each do |list|
|
29
|
+
list.cards.each do |card|
|
30
|
+
c << card
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end.map { |c| Lionel::ProxyCard.new(c) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def spreadsheet
|
37
|
+
@spreadsheet ||= google_session.spreadsheet_by_key(google_doc_id)
|
38
|
+
end
|
39
|
+
|
40
|
+
def worksheet
|
41
|
+
@worksheet ||= Lionel::ProxyWorksheet.new(spreadsheet.worksheets[0])
|
42
|
+
end
|
43
|
+
|
44
|
+
def download
|
45
|
+
puts "Exporting trello board '#{board.name}' (#{trello_board_id}) to " + "google doc '#{spreadsheet.title}' (#{google_doc_id})"
|
46
|
+
|
47
|
+
start_row = 2
|
48
|
+
rows = worksheet.size
|
49
|
+
|
50
|
+
card_rows = {}
|
51
|
+
|
52
|
+
# Find existing rows for current cards
|
53
|
+
(start_row..rows).each do |row|
|
54
|
+
cell_id = worksheet["B",row]
|
55
|
+
next unless cell_id.present?
|
56
|
+
card = cards.find { |c| c.id == cell_id }
|
57
|
+
next unless card.present?
|
58
|
+
card_rows[row] = card
|
59
|
+
end
|
60
|
+
|
61
|
+
# Set available rows for new cards
|
62
|
+
new_cards = cards - card_rows.values
|
63
|
+
new_cards.each_with_index do |card, i|
|
64
|
+
row = rows + i + 1
|
65
|
+
card_rows[row] = card
|
66
|
+
end
|
67
|
+
|
68
|
+
card_rows.each do |row, card|
|
69
|
+
Timeout.timeout(5) { sync_row(row, card) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class CardMap
|
74
|
+
include Enumerable
|
75
|
+
|
76
|
+
attr_reader :cards, :worksheet
|
77
|
+
|
78
|
+
def initialize(cards, worksheet)
|
79
|
+
@cards, @worksheet = cards, worksheet
|
80
|
+
end
|
81
|
+
|
82
|
+
def each(&block)
|
83
|
+
card_rows.each(&block)
|
84
|
+
end
|
85
|
+
|
86
|
+
def card_rows
|
87
|
+
@card_rows ||= {}.tap do |map|
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def upload
|
93
|
+
worksheet.save
|
94
|
+
end
|
95
|
+
|
96
|
+
def rows
|
97
|
+
worksheet.rows
|
98
|
+
end
|
99
|
+
|
100
|
+
def sync_row(row, card)
|
101
|
+
puts "row[#{row}] : #{card.name}"
|
102
|
+
|
103
|
+
worksheet["B",row] = card.id
|
104
|
+
|
105
|
+
# Card link
|
106
|
+
worksheet["C",row] = card.link
|
107
|
+
|
108
|
+
# Ready date
|
109
|
+
ready_action = card.first_action do |a|
|
110
|
+
(a.create? && a.board_id == trello_board_id) || a.moved_to?("Ready")
|
111
|
+
end
|
112
|
+
worksheet["D",row] = card.format_date(ready_action.date) if ready_action
|
113
|
+
|
114
|
+
# In Progress date
|
115
|
+
worksheet["E",row] = card.date_moved_to("In Progress")
|
116
|
+
|
117
|
+
# Code Review date
|
118
|
+
worksheet["F",row] = card.date_moved_to("Code Review")
|
119
|
+
|
120
|
+
# Review date
|
121
|
+
worksheet["G",row] = card.date_moved_to("Review")
|
122
|
+
|
123
|
+
# Deploy date
|
124
|
+
worksheet["H",row] = card.date_moved_to("Deploy")
|
125
|
+
|
126
|
+
# Completed date
|
127
|
+
worksheet["I",row] = card.date_moved_to("Completed")
|
128
|
+
|
129
|
+
# Type
|
130
|
+
worksheet["J",row] = card.type
|
131
|
+
|
132
|
+
# Project
|
133
|
+
worksheet["K",row] = card.project
|
134
|
+
|
135
|
+
# Estimate
|
136
|
+
worksheet["L",row] = card.estimate
|
137
|
+
|
138
|
+
# Due Date
|
139
|
+
worksheet["M",row] = card.due_date
|
140
|
+
|
141
|
+
rescue Trello::Error => e
|
142
|
+
puts e.inspect
|
143
|
+
puts card.inspect
|
144
|
+
end
|
145
|
+
|
146
|
+
def authenticate
|
147
|
+
return if @authenticated
|
148
|
+
trello_session.configure
|
149
|
+
google_session
|
150
|
+
@authenticated
|
151
|
+
end
|
152
|
+
|
153
|
+
def google_session
|
154
|
+
@google_session ||= GoogleDrive.login_with_oauth(configuration.google_token)
|
155
|
+
end
|
156
|
+
|
157
|
+
def trello_session
|
158
|
+
@trello_session ||= TrelloAuthentication.new
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Lionel
|
2
|
+
class GoogleAuthentication
|
3
|
+
include Configurable
|
4
|
+
|
5
|
+
attr_reader :access_token
|
6
|
+
config_accessor :google_client_id, :google_client_secret
|
7
|
+
|
8
|
+
def data
|
9
|
+
raise "No access token" unless access_token
|
10
|
+
{
|
11
|
+
google_token: access_token.token,
|
12
|
+
google_refresh_token: access_token.refresh_token,
|
13
|
+
google_client_id: google_client_id,
|
14
|
+
google_client_secret: google_client_secret
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def retrieve_access_token(authorization_code)
|
19
|
+
@access_token = client.auth_code.get_token(authorization_code,
|
20
|
+
:redirect_uri => "urn:ietf:wg:oauth:2.0:oob")
|
21
|
+
end
|
22
|
+
|
23
|
+
def refresh
|
24
|
+
return false unless refresh_token
|
25
|
+
|
26
|
+
current_token = OAuth2::AccessToken.from_hash(client,
|
27
|
+
{:refresh_token => refresh_token, :expires_at => 36000})
|
28
|
+
@access_token = current_token.refresh! # returns new access_token
|
29
|
+
end
|
30
|
+
|
31
|
+
def authorize_url
|
32
|
+
client.auth_code.authorize_url(
|
33
|
+
:redirect_uri => "urn:ietf:wg:oauth:2.0:oob",
|
34
|
+
:scope =>
|
35
|
+
"https://docs.google.com/feeds/ " +
|
36
|
+
"https://docs.googleusercontent.com/ " +
|
37
|
+
"https://spreadsheets.google.com/feeds/")
|
38
|
+
end
|
39
|
+
|
40
|
+
def api_console_url
|
41
|
+
"https://code.google.com/apis/console"
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def client
|
47
|
+
@client ||= OAuth2::Client.new(google_client_id, google_client_secret,
|
48
|
+
:site => "https://accounts.google.com",
|
49
|
+
:token_url => "/o/oauth2/token",
|
50
|
+
:authorize_url => "/o/oauth2/auth")
|
51
|
+
end
|
52
|
+
|
53
|
+
def refresh_token
|
54
|
+
@refresh_token || configuration.google_refresh_token
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Lionel
|
2
|
+
class ProxyAction
|
3
|
+
attr_reader :action
|
4
|
+
delegate :data, :type, :date, to: :action
|
5
|
+
|
6
|
+
def initialize(action)
|
7
|
+
@action = action
|
8
|
+
end
|
9
|
+
|
10
|
+
def data_attributes(key)
|
11
|
+
data[key] || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def create?
|
15
|
+
type == "createCard"
|
16
|
+
end
|
17
|
+
|
18
|
+
def update?
|
19
|
+
type == "updateCard"
|
20
|
+
end
|
21
|
+
|
22
|
+
def board_id
|
23
|
+
data_attributes("board")["id"]
|
24
|
+
end
|
25
|
+
|
26
|
+
def list_after
|
27
|
+
data_attributes("listAfter")
|
28
|
+
end
|
29
|
+
|
30
|
+
def list_before
|
31
|
+
data_attributes("listBefore")
|
32
|
+
end
|
33
|
+
|
34
|
+
def list_after?
|
35
|
+
list_after.any?
|
36
|
+
end
|
37
|
+
|
38
|
+
def list_before?
|
39
|
+
list_before.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
def moved_to?(list_name)
|
43
|
+
return false unless list_after?
|
44
|
+
!!(list_after["name"] =~ %r{^#{Regexp.escape(list_name.downcase)}}i)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Lionel
|
2
|
+
class ProxyCard
|
3
|
+
attr_reader :card
|
4
|
+
delegate :id, :url, :name, :due, to: :card
|
5
|
+
|
6
|
+
def initialize(card)
|
7
|
+
@card = card
|
8
|
+
end
|
9
|
+
|
10
|
+
def link
|
11
|
+
%Q[=HYPERLINK("#{card.url}", "#{card.name.gsub(/"/, "")}")]
|
12
|
+
end
|
13
|
+
|
14
|
+
def actions
|
15
|
+
@actions ||= card.actions.map { |a| Lionel::ProxyAction.new(a) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def action_date(&block)
|
19
|
+
filtered = actions.select(&block)
|
20
|
+
return "" if filtered.empty?
|
21
|
+
action = filtered.sort { |a, b| a.date <=> b.date }.first
|
22
|
+
format_date action.date
|
23
|
+
end
|
24
|
+
|
25
|
+
def date_moved_to(list_name)
|
26
|
+
action = first_action { |a| a.moved_to?(list_name) }
|
27
|
+
return "" unless action
|
28
|
+
format_date(action.date)
|
29
|
+
end
|
30
|
+
|
31
|
+
def format_date(date, format = "%m/%d/%Y")
|
32
|
+
date.strftime(format)
|
33
|
+
end
|
34
|
+
|
35
|
+
def first_action(&block)
|
36
|
+
actions.select(&block).sort { |a, b| a.date <=> b.date }.first
|
37
|
+
end
|
38
|
+
|
39
|
+
def type
|
40
|
+
labels.detect { |l| l =~ %r{bug|chore|task}i } || 'story'
|
41
|
+
end
|
42
|
+
|
43
|
+
def project
|
44
|
+
labels.detect { |l| l !~ %r{bug|chore|task}i }
|
45
|
+
end
|
46
|
+
|
47
|
+
def labels
|
48
|
+
@labels ||= card.labels.map(&:name).map(&:downcase)
|
49
|
+
end
|
50
|
+
|
51
|
+
def estimate
|
52
|
+
match = card.name.match(/\[(?<estimate>\w)\]/)
|
53
|
+
return "" unless match
|
54
|
+
match[:estimate]
|
55
|
+
end
|
56
|
+
|
57
|
+
def due_date
|
58
|
+
format_date(due) if due
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Lionel
|
2
|
+
class ProxyWorksheet
|
3
|
+
delegate :rows, to: :worksheet
|
4
|
+
delegate :size, to: :rows
|
5
|
+
|
6
|
+
attr_reader :worksheet
|
7
|
+
def initialize(worksheet)
|
8
|
+
@worksheet = worksheet
|
9
|
+
end
|
10
|
+
|
11
|
+
def []=(col, row, value)
|
12
|
+
worksheet["#{col}#{row}"] = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](col, row)
|
16
|
+
worksheet["#{col}#{row}"]
|
17
|
+
end
|
18
|
+
|
19
|
+
HEADER_ROW = 1
|
20
|
+
def content_rows
|
21
|
+
rows(HEADER_ROW)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Lionel
|
2
|
+
class TrelloAuthentication
|
3
|
+
include Configurable
|
4
|
+
|
5
|
+
config_accessor :trello_key, :trello_token
|
6
|
+
|
7
|
+
def data
|
8
|
+
{
|
9
|
+
trello_key: trello_key,
|
10
|
+
trello_token: trello_token
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def configure
|
15
|
+
Trello.configure do |c|
|
16
|
+
c.developer_public_key = trello_key
|
17
|
+
c.member_token = trello_token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def trello_key_url
|
22
|
+
"https://trello.com/1/appKey/generate"
|
23
|
+
end
|
24
|
+
|
25
|
+
def trello_token_url(key = trello_key)
|
26
|
+
"https://trello.com/1/authorize?key=#{key}&name=#{app_name}&response_type=token&scope=read,write,account&expiration=never"
|
27
|
+
end
|
28
|
+
|
29
|
+
def app_name
|
30
|
+
"LionelRichie"
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/lionel_richie.rb
CHANGED
data/lionel_richie.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require '
|
4
|
+
require 'lionel/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "lionel_richie"
|
8
|
-
spec.version =
|
8
|
+
spec.version = Lionel::VERSION
|
9
9
|
spec.authors = ["Ross Kaffenberger"]
|
10
10
|
spec.email = ["rosskaff@gmail.com"]
|
11
11
|
spec.description = %q{Export Trello to Google Docs}
|
@@ -18,11 +18,14 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.bindir = 'bin'
|
22
|
+
spec.executables = %w(lionel)
|
23
|
+
|
21
24
|
spec.add_dependency "ruby-trello"
|
22
25
|
spec.add_dependency "google_drive"
|
23
26
|
spec.add_dependency "yajl-ruby"
|
27
|
+
spec.add_dependency "thor"
|
24
28
|
|
25
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
26
29
|
spec.add_development_dependency "bundler", "~> 1.3"
|
27
30
|
spec.add_development_dependency "launchy"
|
28
31
|
end
|
data/script/console
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
|
5
|
+
# Trap interrupts to quit cleanly. See
|
6
|
+
# https://twitter.com/mitchellh/status/283014103189053442
|
7
|
+
Signal.trap("INT") { exit 1 }
|
8
|
+
|
9
|
+
require 'irb'
|
10
|
+
require 'lionel_richie'
|
11
|
+
|
12
|
+
ARGV.clear # otherwise all script parameters get passed to IRB
|
13
|
+
IRB.start
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lionel_richie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-07-
|
12
|
+
date: 2013-07-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ruby-trello
|
@@ -60,21 +60,21 @@ dependencies:
|
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
62
|
- !ruby/object:Gem::Dependency
|
63
|
-
name:
|
63
|
+
name: thor
|
64
64
|
requirement: !ruby/object:Gem::Requirement
|
65
65
|
none: false
|
66
66
|
requirements:
|
67
|
-
- -
|
67
|
+
- - ! '>='
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
70
|
-
type: :
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
71
|
prerelease: false
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
|
-
- -
|
75
|
+
- - ! '>='
|
76
76
|
- !ruby/object:Gem::Version
|
77
|
-
version: '
|
77
|
+
version: '0'
|
78
78
|
- !ruby/object:Gem::Dependency
|
79
79
|
name: bundler
|
80
80
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,7 +111,6 @@ description: Export Trello to Google Docs
|
|
111
111
|
email:
|
112
112
|
- rosskaff@gmail.com
|
113
113
|
executables:
|
114
|
-
- authorize_lionel
|
115
114
|
- lionel
|
116
115
|
extensions: []
|
117
116
|
extra_rdoc_files: []
|
@@ -121,11 +120,21 @@ files:
|
|
121
120
|
- LICENSE.txt
|
122
121
|
- README.md
|
123
122
|
- Rakefile
|
124
|
-
- bin/authorize_lionel
|
125
123
|
- bin/lionel
|
124
|
+
- lib/lionel.rb
|
125
|
+
- lib/lionel/cli.rb
|
126
|
+
- lib/lionel/configurable.rb
|
127
|
+
- lib/lionel/configuration.rb
|
128
|
+
- lib/lionel/export.rb
|
129
|
+
- lib/lionel/google_authentication.rb
|
130
|
+
- lib/lionel/proxy_action.rb
|
131
|
+
- lib/lionel/proxy_card.rb
|
132
|
+
- lib/lionel/proxy_worksheet.rb
|
133
|
+
- lib/lionel/trello_authentication.rb
|
134
|
+
- lib/lionel/version.rb
|
126
135
|
- lib/lionel_richie.rb
|
127
|
-
- lib/lionel_richie/version.rb
|
128
136
|
- lionel_richie.gemspec
|
137
|
+
- script/console
|
129
138
|
homepage: ''
|
130
139
|
licenses:
|
131
140
|
- MIT
|
data/bin/authorize_lionel
DELETED
@@ -1,61 +0,0 @@
|
|
1
|
-
#! /usr/bin/env ruby
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
-
|
5
|
-
require 'bundler'
|
6
|
-
require 'lionel_richie'
|
7
|
-
Bundler.require(:development)
|
8
|
-
|
9
|
-
commands = []
|
10
|
-
|
11
|
-
puts "Authorize for Trello?"
|
12
|
-
if gets.strip =~ /\Ay/i
|
13
|
-
Launchy.open "https://trello.com/1/appKey/generate"
|
14
|
-
puts "Enter your trello key:"
|
15
|
-
trello_key = gets.strip
|
16
|
-
commands << "export TRELLO_KEY=#{trello_key}"
|
17
|
-
|
18
|
-
Launchy.open "https://trello.com/1/authorize?key=#{trello_key.strip}&name=LionelRichie&response_type=token&scope=read,write,account&expiration=never"
|
19
|
-
puts "Enter your trello token"
|
20
|
-
trello_token = gets.strip
|
21
|
-
commands << "export TRELLO_TOKEN=#{trello_token}"
|
22
|
-
end
|
23
|
-
|
24
|
-
# Google Auth
|
25
|
-
puts "Authorize for Google?"
|
26
|
-
if gets.strip =~ /\Ay/i
|
27
|
-
client = OAuth2::Client.new(
|
28
|
-
ENV['GOOGLE_CLIENT_ID'],
|
29
|
-
ENV['GOOGLE_CLIENT_SECRET'],
|
30
|
-
:site => "https://accounts.google.com",
|
31
|
-
:token_url => "/o/oauth2/token",
|
32
|
-
:authorize_url => "/o/oauth2/auth")
|
33
|
-
|
34
|
-
if !(refresh_token = ENV['GOOGLE_REFRESH_TOKEN'])
|
35
|
-
google_auth_token = OAuth2::AccessToken.from_hash(client,
|
36
|
-
{:refresh_token => refresh_token, :expires_at => 7200})
|
37
|
-
google_auth_token = google_auth_token.refresh!
|
38
|
-
else
|
39
|
-
auth_url = client.auth_code.authorize_url(
|
40
|
-
:redirect_uri => "urn:ietf:wg:oauth:2.0:oob",
|
41
|
-
:scope =>
|
42
|
-
"https://docs.google.com/feeds/ " +
|
43
|
-
"https://docs.googleusercontent.com/ " +
|
44
|
-
"https://spreadsheets.google.com/feeds/")
|
45
|
-
|
46
|
-
# Redirect the user to auth_url and get authorization code from redirect URL.
|
47
|
-
Launchy.open auth_url
|
48
|
-
|
49
|
-
puts "Enter your google key:"
|
50
|
-
authorization_code = gets.strip
|
51
|
-
google_auth_token = client.auth_code.get_token(
|
52
|
-
authorization_code,
|
53
|
-
:redirect_uri => "urn:ietf:wg:oauth:2.0:oob")
|
54
|
-
end
|
55
|
-
|
56
|
-
commands << "export GOOGLE_TOKEN=#{google_auth_token.token}"
|
57
|
-
commands << "export GOOGLE_REFRESH_TOKEN=#{google_auth_token.refresh_token}"
|
58
|
-
end
|
59
|
-
|
60
|
-
puts "Run the following:\n"
|
61
|
-
puts commands
|