lionel_richie 0.0.1 → 0.1.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.
- 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
|