trello_backup_renderer 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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/LICENSE.txt +21 -0
- data/README.md +75 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/scrub_test_data +123 -0
- data/bin/setup +8 -0
- data/exe/trello_backup_renderer +40 -0
- data/lib/trello_backup_renderer.rb +11 -0
- data/lib/trello_backup_renderer/models.rb +83 -0
- data/lib/trello_backup_renderer/parsing.rb +89 -0
- data/lib/trello_backup_renderer/rendering.rb +95 -0
- data/lib/trello_backup_renderer/styles/default.css +58 -0
- data/lib/trello_backup_renderer/styles/no-authorship.css +3 -0
- data/lib/trello_backup_renderer/templates/board.html.erb +55 -0
- data/lib/trello_backup_renderer/version.rb +3 -0
- data/trello_backup_renderer.gemspec +32 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 730791c16eea8bd272b2ed9b8546e21de0eb7b7102da0ae4a101208527b472bf
|
4
|
+
data.tar.gz: 484ff65a7b9caf389cdb16cace8c6284a634908a38664b62c85a65e7c0c9e357
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 215b5edbc15c994be54cec08b91659a60ec5cfc436eaca89b7a30d8704c52b009a51518d017a328f9dbf8af5aa1b3b1a7ed75d1212529f6beb6c7870e8259744
|
7
|
+
data.tar.gz: 87ff3f13fba57368e12b2ca54fb7eda7c01462bf3b51f500b19e4f041eef2c7c312568d0c2d06baa4b4d5496087a844df6fee2b525192f8b3a7f6b15461549f2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.5
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
trello_backup_renderer (0.1.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rake (10.5.0)
|
11
|
+
rspec (3.9.0)
|
12
|
+
rspec-core (~> 3.9.0)
|
13
|
+
rspec-expectations (~> 3.9.0)
|
14
|
+
rspec-mocks (~> 3.9.0)
|
15
|
+
rspec-core (3.9.0)
|
16
|
+
rspec-support (~> 3.9.0)
|
17
|
+
rspec-expectations (3.9.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.9.0)
|
20
|
+
rspec-mocks (3.9.0)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.9.0)
|
23
|
+
rspec-support (3.9.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.17)
|
30
|
+
rake (~> 10.0)
|
31
|
+
rspec (~> 3.0)
|
32
|
+
trello_backup_renderer!
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.17.2
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Jacob Williams
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# trello\_backup\_renderer
|
2
|
+
|
3
|
+
Generates HTML files for [Trello](http://trello.com) boards from backups created by [jtpio/trello-full-backup](https://github.com/jtpio/trello-full-backup).
|
4
|
+
|
5
|
+
* Unlike Trello's built-in print functionality, it includes all card cover images and comments for the board in a single document.
|
6
|
+
* Can be run locally without authorizing a third-party service to access your Trello data.
|
7
|
+
|
8
|
+
You can view a sample file generated by the tool [here](spec/test_board/Renderer%20Test%20Board_full.html).
|
9
|
+
|
10
|
+
(I am not affiliated with Trello in any way.)
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
$ gem install trello_backup_renderer
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
First, use [jtpio/trello-full-backup](https://github.com/jtpio/trello-full-backup) to create a backup of your board.
|
19
|
+
It's important to use the `-t` option.
|
20
|
+
|
21
|
+
Then find the folder in which the backup was created.
|
22
|
+
This should contain a file `BOARD_NAME_full.json`, and folders such as `0_someLongLIstId`.
|
23
|
+
`trello_backup_renderer` uses the full.json file and also uses the files in the nested `attachments` directories, and depends on the folder name format that trello-full-backup generates.
|
24
|
+
(It does not use the `card.json` or `description.md` files that trello-full-backup generates.)
|
25
|
+
|
26
|
+
Run the tool like this:
|
27
|
+
|
28
|
+
$ trello_backup_renderer PATH_TO_BOARD_BACKUP_DIR
|
29
|
+
|
30
|
+
This will create a `BOARD_NAME_full.html` file in the given directory.
|
31
|
+
This file may contain relative links to the attachment files in that directory.
|
32
|
+
|
33
|
+
## Options
|
34
|
+
|
35
|
+
A default stylesheet is embedded into the HTML file; if you don't want it included, use the `--omit-styles` option.
|
36
|
+
You can also link to your own stylesheet using the `--head-insert` option:
|
37
|
+
|
38
|
+
$ trello_backup_renderer PATH_TO_BOARD_BACKUP_DIR --omit-styles --insert-head='<link href="path/to/stylesheet.css" rel="stylesheet">'
|
39
|
+
|
40
|
+
By default, the full name of every comment author is included along with the comment.
|
41
|
+
If the board is private and has only one comment author, you may wish to omit their name.
|
42
|
+
To do this, use the `--hide-authorship` option:
|
43
|
+
|
44
|
+
$ trello_backup_renderer PATH_TO_BOARD_BACKUP_DIR --hide-authorship
|
45
|
+
|
46
|
+
## Supported Features
|
47
|
+
|
48
|
+
Currently only the following data is included in the generated HTML page:
|
49
|
+
|
50
|
+
- Lists
|
51
|
+
- Names
|
52
|
+
- Cards
|
53
|
+
- Covers (when they're attachments)
|
54
|
+
- Names
|
55
|
+
- Labels (name and color)
|
56
|
+
- Descriptions (without formatting)
|
57
|
+
- Comments (without formatting)
|
58
|
+
|
59
|
+
## Development
|
60
|
+
|
61
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
62
|
+
|
63
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
64
|
+
|
65
|
+
A script `bin/scrub_test_data` is included which can be run against a board backup directory to remove attributes and files which are unused by trello_backup_renderer and replace IDs and member names with placeholder values.
|
66
|
+
This was used in creating the test data seen in `spec/test_board`.
|
67
|
+
Warning: The `scrub_test_data` script deletes data from the specified directory in-place.
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
|
71
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/brokensandals/trello_backup_renderer.
|
72
|
+
|
73
|
+
## License
|
74
|
+
|
75
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "trello_backup_renderer"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/scrub_test_data
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
unless ARGV.count == 1
|
6
|
+
puts "Usage: scrub_test_data BOARD_DIRECTORY"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
|
10
|
+
RULES = {
|
11
|
+
'actions' => {
|
12
|
+
'data' => {
|
13
|
+
'card' => {
|
14
|
+
'id' => :id
|
15
|
+
},
|
16
|
+
'text' => true
|
17
|
+
},
|
18
|
+
'date' => true,
|
19
|
+
'memberCreator' => {
|
20
|
+
'fullName' => :memberFullName
|
21
|
+
},
|
22
|
+
'type' => true
|
23
|
+
},
|
24
|
+
'cards' => {
|
25
|
+
'name' => true,
|
26
|
+
'closed' => true,
|
27
|
+
'cover' => {
|
28
|
+
'idAttachment' => :id
|
29
|
+
},
|
30
|
+
'desc' => true,
|
31
|
+
'id' => :id,
|
32
|
+
'idList' => :id,
|
33
|
+
'labels' => ['color', 'name'],
|
34
|
+
'pos' => true
|
35
|
+
},
|
36
|
+
'lists' => {
|
37
|
+
'closed' => true,
|
38
|
+
'id' => :id,
|
39
|
+
'name' => true,
|
40
|
+
'pos' => true
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
ANONYMIZATION_MAPPINGS = {}
|
45
|
+
ANONYMIZATION_VALUES = {
|
46
|
+
id: (1..100).map { |i| "id#{i}"},
|
47
|
+
memberFullName: ['Smerson McPerson']
|
48
|
+
}
|
49
|
+
|
50
|
+
def anonymize(value, type)
|
51
|
+
mappings = (ANONYMIZATION_MAPPINGS[type] ||= {})
|
52
|
+
mappings[value] ||= ((ANONYMIZATION_VALUES[type] || []).shift or raise "Ran out of anonymizations for #{type}")
|
53
|
+
end
|
54
|
+
|
55
|
+
def scrub_json(json, rules)
|
56
|
+
if json.is_a?(Array)
|
57
|
+
json.each { |item| scrub_json(item, rules) }
|
58
|
+
elsif json.is_a?(Hash)
|
59
|
+
json.keys.each do |key|
|
60
|
+
if rules.include?(key)
|
61
|
+
if rules.is_a?(Hash)
|
62
|
+
case rules[key]
|
63
|
+
when Array, Hash
|
64
|
+
scrub_json(json[key], rules[key])
|
65
|
+
when Symbol
|
66
|
+
json[key] = anonymize(json[key], rules[key])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
json.delete(key)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def scrub_dir(dir)
|
77
|
+
board_file_paths = Dir.glob("#{dir}/*.json").to_a
|
78
|
+
raise "Expected exactly one json file in #{dir}" unless board_file_paths.count == 1
|
79
|
+
|
80
|
+
board_json = JSON.parse(File.read(board_file_paths.first))
|
81
|
+
scrub_json(board_json, RULES)
|
82
|
+
File.write(board_file_paths.first, JSON.pretty_generate(board_json))
|
83
|
+
|
84
|
+
Dir.glob("#{dir}/*_*/*_*/attachments/*.*").to_a.each do |attachment_path|
|
85
|
+
basename = File.basename(attachment_path)
|
86
|
+
if match = /\A(?<pos>\d+)_(?<id>[a-z0-9]+)(?<rest>.*)?\z/.match(basename)
|
87
|
+
new_id = anonymize(match[:id], :id)
|
88
|
+
new_path = File.join(File.dirname(attachment_path), "#{match[:pos]}_#{new_id}#{match[:rest]}")
|
89
|
+
File.rename(attachment_path, new_path)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
File.delete(*Dir.glob("#{dir}/**/card.json").to_a)
|
94
|
+
File.delete(*Dir.glob("#{dir}/**/description.md").to_a)
|
95
|
+
|
96
|
+
Dir.glob("#{dir}/*_*/*_*").to_a.each do |card_dir_path|
|
97
|
+
next unless File.directory?(card_dir_path)
|
98
|
+
if Dir.empty?(card_dir_path)
|
99
|
+
Dir.rmdir(card_dir_path)
|
100
|
+
else
|
101
|
+
basename = File.basename(card_dir_path)
|
102
|
+
pos, id = basename.split('_')
|
103
|
+
new_id = anonymize(id, :id)
|
104
|
+
new_path = File.join(File.dirname(card_dir_path), "#{pos}_#{new_id}")
|
105
|
+
File.rename(card_dir_path, new_path)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
Dir.glob("#{dir}/*_*").to_a.each do |list_dir_path|
|
110
|
+
next unless File.directory?(list_dir_path)
|
111
|
+
if Dir.empty?(list_dir_path)
|
112
|
+
Dir.rmdir(list_dir_path)
|
113
|
+
else
|
114
|
+
basename = File.basename(list_dir_path)
|
115
|
+
pos, id = basename.split('_')
|
116
|
+
new_id = anonymize(id, :id)
|
117
|
+
new_path = File.join(File.dirname(list_dir_path), "#{pos}_#{new_id}")
|
118
|
+
File.rename(list_dir_path, new_path)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
scrub_dir(ARGV[0])
|
data/bin/setup
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'trello_backup_renderer'
|
5
|
+
|
6
|
+
options = {}
|
7
|
+
optparse = OptionParser.new do |opts|
|
8
|
+
opts.banner = 'Usage: trello_backup_renderer BOARD_BACKUP_DIR [options]'
|
9
|
+
|
10
|
+
opts.on('--head-insert=HTML', 'Add HTML inside the <head> tag') do |html|
|
11
|
+
options[:head_insert] = html
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on('--hide-authorship', 'Do not display the authors of comments. (Done via CSS.)') do
|
15
|
+
options[:hide_authorship] = true
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on('--omit-styles', 'Do not include default stylesheet.') do
|
19
|
+
options[:omit_styles] = true
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on('--version', 'Print program version and exit') do
|
23
|
+
puts "trello_backup_renderer #{TrelloBackupRenderer::VERSION}"
|
24
|
+
exit 0
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
optparse.parse!
|
29
|
+
if ARGV.length != 1
|
30
|
+
puts optparse
|
31
|
+
exit -1
|
32
|
+
end
|
33
|
+
|
34
|
+
rendering_options = TrelloBackupRenderer::Rendering::Options.new(options)
|
35
|
+
|
36
|
+
board_dir = ARGV.shift
|
37
|
+
board = TrelloBackupRenderer.load_board_dir(board_dir)
|
38
|
+
html = TrelloBackupRenderer.generate_board_html(board, rendering_options)
|
39
|
+
out_file = File.join(board_dir, File.basename(TrelloBackupRenderer.find_board_json_file(board_dir)).sub(/json\z/, 'html'))
|
40
|
+
File.write(out_file, html)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'trello_backup_renderer/version'
|
2
|
+
require 'trello_backup_renderer/models'
|
3
|
+
require 'trello_backup_renderer/parsing'
|
4
|
+
require 'trello_backup_renderer/rendering'
|
5
|
+
|
6
|
+
module TrelloBackupRenderer
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
extend Parsing
|
10
|
+
extend Rendering
|
11
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module TrelloBackupRenderer
|
2
|
+
module Models
|
3
|
+
class Board
|
4
|
+
attr_reader :lists
|
5
|
+
|
6
|
+
def initialize(args = {})
|
7
|
+
@lists = (args[:lists] || []).sort_by(&:pos)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class List
|
12
|
+
attr_reader :cards
|
13
|
+
attr_reader :closed
|
14
|
+
attr_reader :id
|
15
|
+
attr_reader :name
|
16
|
+
attr_reader :pos
|
17
|
+
|
18
|
+
def initialize(args)
|
19
|
+
@cards = (args[:cards] || []).sort_by(&:pos)
|
20
|
+
@closed = args[:closed]
|
21
|
+
@id = args[:id]
|
22
|
+
@name = args[:name]
|
23
|
+
@pos = args[:pos]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Card
|
28
|
+
attr_reader :closed
|
29
|
+
attr_reader :comments
|
30
|
+
attr_reader :cover_attachment
|
31
|
+
attr_reader :desc
|
32
|
+
attr_reader :id
|
33
|
+
attr_reader :id_list
|
34
|
+
attr_reader :labels
|
35
|
+
attr_reader :name
|
36
|
+
attr_reader :pos
|
37
|
+
|
38
|
+
def initialize(args)
|
39
|
+
@closed = args[:closed]
|
40
|
+
@comments = (args[:comments] || []).sort_by(&:date)
|
41
|
+
@cover_attachment = args[:cover_attachment]
|
42
|
+
@desc = args[:desc]
|
43
|
+
@id = args[:id]
|
44
|
+
@id_list = args[:id_list]
|
45
|
+
@labels = (args[:labels] || []).sort_by(&:name)
|
46
|
+
@name = args[:name]
|
47
|
+
@pos = args[:pos]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Attachment
|
52
|
+
attr_reader :path
|
53
|
+
|
54
|
+
def initialize(args)
|
55
|
+
@path = args[:path]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Label
|
60
|
+
attr_reader :color
|
61
|
+
attr_reader :name
|
62
|
+
|
63
|
+
def initialize(args)
|
64
|
+
@color = args[:color]
|
65
|
+
@name = args[:name]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Comment
|
70
|
+
attr_reader :creator_full_name
|
71
|
+
attr_reader :date
|
72
|
+
attr_reader :id_card
|
73
|
+
attr_reader :text
|
74
|
+
|
75
|
+
def initialize(args)
|
76
|
+
@creator_full_name = args[:creator_full_name]
|
77
|
+
@date = args[:date]
|
78
|
+
@id_card = args[:id_card]
|
79
|
+
@text = args[:text]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module TrelloBackupRenderer
|
4
|
+
module Parsing
|
5
|
+
include Models
|
6
|
+
|
7
|
+
def parse_comment_json(action_json)
|
8
|
+
return nil unless action_json['type'] == 'commentCard'
|
9
|
+
Comment.new(
|
10
|
+
creator_full_name: action_json['memberCreator']['fullName'],
|
11
|
+
date: action_json['date'],
|
12
|
+
id_card: action_json['data']['card']['id'],
|
13
|
+
text: action_json['data']['text']
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_label_json(label_json)
|
18
|
+
Label.new(
|
19
|
+
color: label_json['color'],
|
20
|
+
name: label_json['name']
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_card_json(card_json, attachments_by_id, comments)
|
25
|
+
cover_id_attachment = card_json.fetch('cover', {})['idAttachment']
|
26
|
+
|
27
|
+
Card.new(
|
28
|
+
closed: card_json['closed'],
|
29
|
+
comments: comments.select { |comment| comment.id_card == card_json['id'] },
|
30
|
+
cover_attachment: attachments_by_id[cover_id_attachment],
|
31
|
+
desc: (card_json['desc'] if card_json['desc'] && !card_json['desc'].empty?),
|
32
|
+
id: card_json['id'],
|
33
|
+
id_list: card_json['idList'],
|
34
|
+
labels: (card_json['labels'] || []).map { |label_json| parse_label_json(label_json) },
|
35
|
+
name: card_json['name'],
|
36
|
+
pos: card_json['pos']
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_list_json(list_json, cards)
|
41
|
+
List.new(
|
42
|
+
cards: (cards.select { |card| card.id_list == list_json['id'] }),
|
43
|
+
closed: list_json['closed'],
|
44
|
+
id: list_json['id'],
|
45
|
+
name: list_json['name'],
|
46
|
+
pos: list_json['pos']
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse_board_json(board_json, attachment_paths)
|
51
|
+
attachments_by_id = attachment_paths.transform_values { |path| Attachment.new(path: path) }
|
52
|
+
comments = (board_json['actions'] || []).map { |action_json| parse_comment_json(action_json) }.compact
|
53
|
+
cards = (board_json['cards'] || []).map { |card_json| parse_card_json(card_json, attachments_by_id, comments) }
|
54
|
+
Board.new(
|
55
|
+
lists: (board_json['lists'] || []).map { |list_json| parse_list_json(list_json, cards) }
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_attachment_paths(dir)
|
60
|
+
paths = {}
|
61
|
+
Dir.glob(File.join(dir, '*_*', '*_*', 'attachments', '*')) do |path|
|
62
|
+
if File.file?(path) && match = /\A\d+_(?<id>[a-z0-9]+).*\z/.match(File.basename(path))
|
63
|
+
# I'm just assuming the attachment IDs are globally unique rather than scoped to the card,
|
64
|
+
# although I wasn't able to confirm this in the API docs.
|
65
|
+
paths[match[:id]] = path
|
66
|
+
end
|
67
|
+
end
|
68
|
+
paths
|
69
|
+
end
|
70
|
+
|
71
|
+
def relativize_attachment_paths(paths, base_dir)
|
72
|
+
paths.transform_values { |path| path.sub(/\A#{Regexp.quote(base_dir)}\/?/, '')}
|
73
|
+
end
|
74
|
+
|
75
|
+
def find_board_json_file(dir)
|
76
|
+
board_file_paths = Dir.glob(File.join(dir, '*_full.json')).to_a
|
77
|
+
raise Error, "Expected one <board_name>_full.json file in: #{dir}" unless board_file_paths.count == 1
|
78
|
+
board_file_paths.first
|
79
|
+
end
|
80
|
+
|
81
|
+
def load_board_dir(dir)
|
82
|
+
raise Error, "Not a directory: #{dir}" unless File.directory?(dir)
|
83
|
+
board_file_path = find_board_json_file(dir)
|
84
|
+
board_json = JSON.parse(File.read(board_file_path))
|
85
|
+
attachment_paths = relativize_attachment_paths(load_attachment_paths(dir), dir)
|
86
|
+
parse_board_json(board_json, attachment_paths)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module TrelloBackupRenderer
|
4
|
+
module Rendering
|
5
|
+
BOARD_HTML_TEMPLATE = File.read(File.join(__dir__, 'templates', 'board.html.erb'))
|
6
|
+
CSS_FILE = File.read(File.join(__dir__, 'styles', 'default.css'))
|
7
|
+
NO_AUTHORSHIP_CSS_FILE = File.read(File.join(__dir__, 'styles', 'no-authorship.css'))
|
8
|
+
|
9
|
+
class Options
|
10
|
+
attr_reader :hide_authorship
|
11
|
+
attr_reader :omit_styles
|
12
|
+
attr_reader :head_insert
|
13
|
+
|
14
|
+
def initialize(args = {})
|
15
|
+
@hide_authorship = args[:hide_authorship]
|
16
|
+
@omit_styles = args[:omit_styles]
|
17
|
+
@head_insert = args[:head_insert]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class BoardPage
|
22
|
+
def initialize(board, options)
|
23
|
+
@board = board
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
def lists
|
28
|
+
@board.lists.reject(&:closed).map { |list| ListPresenter.new(list) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def stylesheet_tags
|
32
|
+
tags = ''
|
33
|
+
tags << '<style type="text/css">' + CSS_FILE + '</style>' unless @options.omit_styles
|
34
|
+
tags << '<style type="text/css">' + NO_AUTHORSHIP_CSS_FILE + '</style>' if @options.hide_authorship
|
35
|
+
tags
|
36
|
+
end
|
37
|
+
|
38
|
+
def head_insert
|
39
|
+
@options.head_insert || ''
|
40
|
+
end
|
41
|
+
|
42
|
+
def render
|
43
|
+
ERB.new(BOARD_HTML_TEMPLATE).result(binding)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class ListPresenter < SimpleDelegator
|
48
|
+
def cards
|
49
|
+
super.reject(&:closed).map { |card| CardPresenter.new(card) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def name
|
53
|
+
ERB::Util.h(super)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class CardPresenter < SimpleDelegator
|
58
|
+
def desc
|
59
|
+
ERB::Util.h(super) if super
|
60
|
+
end
|
61
|
+
|
62
|
+
def comments
|
63
|
+
super.map { |comment| CommentPresenter.new(comment) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def labels
|
67
|
+
super.map { |label| LabelPresenter.new(label) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def name
|
71
|
+
ERB::Util.h(super)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class LabelPresenter < SimpleDelegator
|
76
|
+
def name
|
77
|
+
ERB::Util.h(super)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class CommentPresenter < SimpleDelegator
|
82
|
+
def creator_full_name
|
83
|
+
ERB::Util.h(super)
|
84
|
+
end
|
85
|
+
|
86
|
+
def text
|
87
|
+
ERB::Util.h(super)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def generate_board_html(board, options = Options.new)
|
92
|
+
BoardPage.new(board, options).render
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
.lists {
|
2
|
+
padding-left: 0px;
|
3
|
+
}
|
4
|
+
|
5
|
+
.list {
|
6
|
+
list-style-type: none;
|
7
|
+
}
|
8
|
+
|
9
|
+
.list-name {
|
10
|
+
padding-bottom: 5px;
|
11
|
+
border-bottom: 1px solid black;
|
12
|
+
}
|
13
|
+
|
14
|
+
.cards {
|
15
|
+
padding-left: 5px;
|
16
|
+
}
|
17
|
+
|
18
|
+
.card {
|
19
|
+
width: 300px;
|
20
|
+
list-style-type: none;
|
21
|
+
border: 1px solid gray;
|
22
|
+
border-radius: 3px;
|
23
|
+
margin-bottom: 10px;
|
24
|
+
padding: 10px;
|
25
|
+
}
|
26
|
+
|
27
|
+
.card-cover {
|
28
|
+
object-fit: scale-down;
|
29
|
+
margin: 0;
|
30
|
+
}
|
31
|
+
|
32
|
+
.card-name {
|
33
|
+
font-size: 120%;
|
34
|
+
margin: 0;
|
35
|
+
padding: 0;
|
36
|
+
}
|
37
|
+
|
38
|
+
.labels {
|
39
|
+
margin-top: 5px;
|
40
|
+
padding-left: 0;
|
41
|
+
}
|
42
|
+
|
43
|
+
.label {
|
44
|
+
list-style-type: none;
|
45
|
+
display: inline-block;
|
46
|
+
padding: 3px;
|
47
|
+
border-radius: 3px;
|
48
|
+
font-size: 75%;
|
49
|
+
}
|
50
|
+
|
51
|
+
.comments {
|
52
|
+
padding-left: 0;
|
53
|
+
}
|
54
|
+
|
55
|
+
.comment {
|
56
|
+
list-style-type: none;
|
57
|
+
border-top: 1px solid gray;
|
58
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8"/>
|
5
|
+
|
6
|
+
<%= stylesheet_tags %>
|
7
|
+
|
8
|
+
<%= head_insert %>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
<ul class="lists">
|
13
|
+
<% lists.each do |list| %>
|
14
|
+
<li class="list">
|
15
|
+
<h1 class="list-name"><%= list.name %></h1>
|
16
|
+
|
17
|
+
<ul class="cards">
|
18
|
+
<% list.cards.each do |card| %>
|
19
|
+
<li class="card">
|
20
|
+
<% if card.cover_attachment %>
|
21
|
+
<img class="card-cover" src="<%= card.cover_attachment.path %>" width="300px"/>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<h2 class="card-name"><%= card.name %></h2>
|
25
|
+
|
26
|
+
<% if card.labels.any? %>
|
27
|
+
<ul class="labels">
|
28
|
+
<% card.labels.each do |label| %>
|
29
|
+
<li class="label" style="background-color: <%= label.color %>"><%= label.name %></li>
|
30
|
+
<% end %>
|
31
|
+
</ul>
|
32
|
+
<% end %>
|
33
|
+
|
34
|
+
<% if card.desc %>
|
35
|
+
<p class="card-desc"><%= card.desc %></p>
|
36
|
+
<% end %>
|
37
|
+
|
38
|
+
<% if card.comments.any? %>
|
39
|
+
<ul class="comments">
|
40
|
+
<% card.comments.each do |comment| %>
|
41
|
+
<li class="comment">
|
42
|
+
<p><span class="authorship"><%= comment.creator_full_name %> commented:</span> <%= comment.text %></p>
|
43
|
+
</li>
|
44
|
+
<% end %>
|
45
|
+
</ul>
|
46
|
+
<% end %>
|
47
|
+
</li>
|
48
|
+
<% end %>
|
49
|
+
</ul>
|
50
|
+
</li>
|
51
|
+
<% end %>
|
52
|
+
</ul>
|
53
|
+
|
54
|
+
</body>
|
55
|
+
</html>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "trello_backup_renderer/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "trello_backup_renderer"
|
8
|
+
spec.version = TrelloBackupRenderer::VERSION
|
9
|
+
spec.authors = ["Jacob Williams"]
|
10
|
+
spec.email = ["jacobaw@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Renders Trello backups to HTML files. Operates on backups created by https://github.com/jtpio/trello-full-backup}
|
13
|
+
spec.homepage = "https://github.com/brokensandals/trello_backup_renderer"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/brokensandals/trello_backup_renderer"
|
18
|
+
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
30
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trello_backup_renderer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jacob Williams
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.17'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.17'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- jacobaw@gmail.com
|
58
|
+
executables:
|
59
|
+
- trello_backup_renderer
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".rspec"
|
65
|
+
- ".ruby-version"
|
66
|
+
- ".travis.yml"
|
67
|
+
- Gemfile
|
68
|
+
- Gemfile.lock
|
69
|
+
- LICENSE.txt
|
70
|
+
- README.md
|
71
|
+
- Rakefile
|
72
|
+
- bin/console
|
73
|
+
- bin/scrub_test_data
|
74
|
+
- bin/setup
|
75
|
+
- exe/trello_backup_renderer
|
76
|
+
- lib/trello_backup_renderer.rb
|
77
|
+
- lib/trello_backup_renderer/models.rb
|
78
|
+
- lib/trello_backup_renderer/parsing.rb
|
79
|
+
- lib/trello_backup_renderer/rendering.rb
|
80
|
+
- lib/trello_backup_renderer/styles/default.css
|
81
|
+
- lib/trello_backup_renderer/styles/no-authorship.css
|
82
|
+
- lib/trello_backup_renderer/templates/board.html.erb
|
83
|
+
- lib/trello_backup_renderer/version.rb
|
84
|
+
- trello_backup_renderer.gemspec
|
85
|
+
homepage: https://github.com/brokensandals/trello_backup_renderer
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata:
|
89
|
+
homepage_uri: https://github.com/brokensandals/trello_backup_renderer
|
90
|
+
source_code_uri: https://github.com/brokensandals/trello_backup_renderer
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubygems_version: 3.0.3
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Renders Trello backups to HTML files. Operates on backups created by https://github.com/jtpio/trello-full-backup
|
110
|
+
test_files: []
|