minglr 1.3.11
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/.document +5 -0
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/PostInstall.txt +1 -0
- data/README.rdoc +87 -0
- data/Rakefile +90 -0
- data/VERSION.yml +4 -0
- data/bin/minglr +29 -0
- data/bin/mtx +14 -0
- data/cucumber.yml +2 -0
- data/features/cards.feature +27 -0
- data/features/step_definitions/minglr_steps.rb +34 -0
- data/features/step_definitions/shared_steps.rb +69 -0
- data/features/users.feature +6 -0
- data/lib/minglr.rb +34 -0
- data/lib/minglr/action.rb +85 -0
- data/lib/minglr/config_parser.rb +51 -0
- data/lib/minglr/extensions/array.rb +23 -0
- data/lib/minglr/mtx/input_cache.rb +24 -0
- data/lib/minglr/mtx/options_parser.rb +58 -0
- data/lib/minglr/options_parser.rb +82 -0
- data/lib/minglr/resources/attachment.rb +51 -0
- data/lib/minglr/resources/base.rb +42 -0
- data/lib/minglr/resources/card.rb +117 -0
- data/lib/minglr/resources/project.rb +25 -0
- data/lib/minglr/resources/property_definition.rb +12 -0
- data/lib/minglr/resources/transition_execution.rb +6 -0
- data/lib/minglr/resources/user.rb +19 -0
- data/minglr.gemspec +120 -0
- data/minglrconfig.sample +37 -0
- data/tasks/commit.sample.rake +86 -0
- data/tasks/svn.sample.rake +27 -0
- data/test/action_test.rb +75 -0
- data/test/commands_test.rb +111 -0
- data/test/config_parser_test.rb +116 -0
- data/test/extensions/array_test.rb +41 -0
- data/test/options_parser_test.rb +74 -0
- data/test/resources/attachment_test.rb +90 -0
- data/test/resources/base_test.rb +58 -0
- data/test/resources/card_test.rb +199 -0
- data/test/resources/project_test.rb +44 -0
- data/test/resources/property_definition_test.rb +25 -0
- data/test/resources/user_test.rb +32 -0
- data/test/test_helper.rb +31 -0
- metadata +219 -0
@@ -0,0 +1,85 @@
|
|
1
|
+
module Minglr
|
2
|
+
|
3
|
+
class Action
|
4
|
+
|
5
|
+
def self.execute(action, options = [], flag_options = {}, config = {})
|
6
|
+
if action.to_s == options[0]
|
7
|
+
options.shift
|
8
|
+
else
|
9
|
+
options.shift
|
10
|
+
options.shift
|
11
|
+
end
|
12
|
+
begin
|
13
|
+
Commands.send(action, options, flag_options, config)
|
14
|
+
rescue ActiveResource::ResourceNotFound => error
|
15
|
+
puts error.message + " for URL '#{Resources::Base.site}' ..."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.valid_actions
|
20
|
+
Commands.methods(false)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.valid_action?(action)
|
24
|
+
valid_actions.include? action
|
25
|
+
end
|
26
|
+
|
27
|
+
class Commands
|
28
|
+
|
29
|
+
def self.attach(options, flag_options, config)
|
30
|
+
raise "Missing card number!" if options.empty?
|
31
|
+
|
32
|
+
card_number = options.first
|
33
|
+
file_name = flag_options[:file_attachment]
|
34
|
+
Resources::Attachment.attach(card_number, file_name, config[:username], config[:password])
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.card(options, flag_options, config)
|
38
|
+
raise "Missing card number!" if options.empty?
|
39
|
+
|
40
|
+
card_number = options.first
|
41
|
+
Resources::Card.print_card(card_number, config[:status_property])
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.cards(options, flag_options, config)
|
45
|
+
Resources::Card.print_all(options, config[:status_property])
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.create(options, flag_options, config)
|
49
|
+
Resources::Card.create(flag_options, config[:status_property])
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.fetch(options, flag_options, config)
|
53
|
+
raise "Missing card number!" if options.empty?
|
54
|
+
|
55
|
+
card_number = options.first
|
56
|
+
Resources::Attachment.fetch(card_number, config[:username], config[:password])
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.move(options, flag_options, config)
|
60
|
+
raise "Missing card number!" if options.empty?
|
61
|
+
|
62
|
+
card_number = options.first
|
63
|
+
Resources::Card.move(card_number, flag_options, config)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.projects(options, flag_options, config)
|
67
|
+
Resources::Project.print_all(options, config[:status_property])
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.update(options, flag_options, config)
|
71
|
+
raise "Missing card number!" if options.empty?
|
72
|
+
|
73
|
+
card_number = options.first
|
74
|
+
Resources::Card.update(card_number, flag_options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.users(options, flag_options, config)
|
78
|
+
Resources::User.print_all(options)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Minglr
|
2
|
+
|
3
|
+
class ConfigParser
|
4
|
+
|
5
|
+
CONFIG_FILE = ".minglrconfig"
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def self.parse
|
9
|
+
config_files = [File.join(ENV["HOME"], CONFIG_FILE), File.join(ENV["PWD"], CONFIG_FILE)]
|
10
|
+
config_files.uniq!
|
11
|
+
config_files.each do |config_file_name|
|
12
|
+
if File.exist?(config_file_name)
|
13
|
+
return self.new(File.read(config_file_name)).config
|
14
|
+
end
|
15
|
+
end
|
16
|
+
puts "Unable to find #{CONFIG_FILE} in #{config_files.join(", ")}"
|
17
|
+
class_eval("send :exit, 1") # Why is it so hard to mock or stub exit?
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(config_contents)
|
21
|
+
@config = {}
|
22
|
+
@current_section = nil
|
23
|
+
config_contents.each_line do |line|
|
24
|
+
line = line.strip!
|
25
|
+
case line
|
26
|
+
when "", /^#.*$/
|
27
|
+
next
|
28
|
+
when /\[(.*)\]/
|
29
|
+
define_section($1.to_s)
|
30
|
+
else
|
31
|
+
define_var(line)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@config
|
35
|
+
end
|
36
|
+
|
37
|
+
def define_section(section_name)
|
38
|
+
@config[section_name.to_sym] = {} unless @config.has_key?(section_name.to_sym)
|
39
|
+
@current_section = section_name.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def define_var(line)
|
43
|
+
key, value = line.split("=")
|
44
|
+
key.strip!
|
45
|
+
value.strip!
|
46
|
+
@config[@current_section][key.to_sym] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Minglr
|
2
|
+
|
3
|
+
module Extensions
|
4
|
+
|
5
|
+
module Array
|
6
|
+
|
7
|
+
def filter(attributes, words)
|
8
|
+
collection = self
|
9
|
+
words.each do |word|
|
10
|
+
collection = self.select do |element|
|
11
|
+
output = ""
|
12
|
+
attributes.each { |attribute| output << element.send(attribute).to_s + " " }
|
13
|
+
output =~ /#{word}/i
|
14
|
+
end
|
15
|
+
end
|
16
|
+
collection
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module MTX
|
2
|
+
class InputCache
|
3
|
+
class << self
|
4
|
+
def put(key, content)
|
5
|
+
File.open(file_pathname(key), File::CREAT | File::WRONLY | File::TRUNC) { |file| file.write content }
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(key)
|
9
|
+
if content = File.read(file_pathname(key))
|
10
|
+
return nil if content.blank?
|
11
|
+
content
|
12
|
+
end
|
13
|
+
rescue
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def file_pathname(key)
|
20
|
+
File.join(Dir::tmpdir, "#{key.to_s.gsub(/[^\w]/, '')}.entry")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module MTX
|
2
|
+
|
3
|
+
class OptionsParser
|
4
|
+
|
5
|
+
def self.parse(args, *required_by_command)
|
6
|
+
uri_options = {}
|
7
|
+
command_options = {}
|
8
|
+
|
9
|
+
parser = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: mtx [options]"
|
11
|
+
opts.on("--transition TRANSITION", "Transition name.") do |transition|
|
12
|
+
command_options[:transition] = transition
|
13
|
+
end
|
14
|
+
|
15
|
+
opts.on("--card CARD", "Card number.") do |card|
|
16
|
+
command_options[:card] = card
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on("--properties ARGS", Array, "User-entered properties and values for the transition in array format. Must be an even number of comma-delimited values, like \"A,B,'C with spaces','D with spaces'\".") do |args|
|
20
|
+
command_options[:properties] = args.in_groups_of(2).map { |key, value| {'name' => key, 'value' => value} }
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("--comment COMMENT", "Transition comment. This may be required depending on your transition settings.") do |comment|
|
24
|
+
command_options[:comment] = comment
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("--username USERNAME", "Mingle username.") do |username|
|
28
|
+
uri_options[:username] = username
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("--password PASSWORD", "Mingle password.") do |password|
|
32
|
+
uri_options[:password] = password
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("--host_port HOST_PORT", "Host and port.") do |host_and_port|
|
36
|
+
uri_options[:host_and_port] = host_and_port
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("--project PROJECT", "Project name.") do |project|
|
40
|
+
uri_options[:project] = project
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
parser.parse! args
|
45
|
+
|
46
|
+
([:project, :host_and_port] | required_by_command).each do |arg|
|
47
|
+
unless command_options[arg] || uri_options[arg]
|
48
|
+
# TODO: let commands handle their own errors
|
49
|
+
$stderr.puts "Missing command-line argument --#{arg.to_s}, use --help for command-line options."
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
[uri_options, command_options]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Minglr
|
4
|
+
class OptionsParser
|
5
|
+
def self.parse(args, *required_by_command)
|
6
|
+
project_options = []
|
7
|
+
|
8
|
+
if Resources::Base.site
|
9
|
+
begin
|
10
|
+
project_options = Resources::PropertyDefinition.project_options
|
11
|
+
rescue ActiveResource::UnauthorizedAccess => exception
|
12
|
+
puts "Connection #{exception.message} to #{Resources::Base.site.to_s}"
|
13
|
+
puts "Did you set 'basic_authentication_enabled: true' in your auth_config.yml file?"
|
14
|
+
exit 1 unless MINGLR_ENV == "test"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
command_options = {}
|
19
|
+
|
20
|
+
parser = OptionParser.new do |opts|
|
21
|
+
opts.banner = "Usage: minglr [action] [options]"
|
22
|
+
opts.separator ""
|
23
|
+
opts.separator "Valid Commands Are: #{Minglr::Action.valid_actions.join(", ")}"
|
24
|
+
|
25
|
+
opts.on("-n NAME", String, "Short name of card") do |card_name|
|
26
|
+
command_options[:name] = card_name.strip
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-d DESCRIPTION", String, "Description of card") do |card_description|
|
30
|
+
command_options[:description] = card_description.strip
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-t TYPE", String, "Type of card") do |card_type|
|
34
|
+
command_options[:card_type_name] = card_type.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-c COMMENT", String, "Comment") do |comment|
|
38
|
+
command_options[:comment] = comment.strip
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-f FILE", String, "File to attach") do |file|
|
42
|
+
command_options[:file_attachment] = file.strip
|
43
|
+
end
|
44
|
+
|
45
|
+
unless project_options.empty?
|
46
|
+
opts.separator ""
|
47
|
+
opts.separator "Project Specific Options"
|
48
|
+
|
49
|
+
project_options.each do |option_pair|
|
50
|
+
option_switch = option_pair[1].downcase.gsub(/\s|_/, "-").gsub(/--|---/, '-').gsub(/--/, '-')
|
51
|
+
opts.on("--#{option_switch}", String, "Set the #{option_pair[1]} for a card") do |option|
|
52
|
+
command_options[option_pair[0]] = option
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.separator ""
|
57
|
+
end
|
58
|
+
|
59
|
+
opts.on_tail("-h", "--help", "Show this help message.") do |help|
|
60
|
+
puts opts
|
61
|
+
exit 0 unless MINGLR_ENV == "test"
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on_tail("--version", "Show version") do
|
65
|
+
version = YAML.load(File.read(File.join(File.dirname(__FILE__), "..", "..", "VERSION.yml")))
|
66
|
+
puts "#{version[:major]}.#{version[:minor]}.#{version[:patch]}"
|
67
|
+
exit 0 unless MINGLR_ENV == "test"
|
68
|
+
end
|
69
|
+
|
70
|
+
if args.empty?
|
71
|
+
puts opts
|
72
|
+
exit 0 unless MINGLR_ENV == "test"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
parser.parse! args
|
77
|
+
command_options
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'httpclient'
|
2
|
+
|
3
|
+
module Resources
|
4
|
+
|
5
|
+
class Attachment < Base
|
6
|
+
|
7
|
+
def self.configure
|
8
|
+
self.prefix += "cards/:card_number/"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.curl(command)
|
12
|
+
`#{command}`
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.fetch(card_number, username, password)
|
16
|
+
if card_to_update = Card.find(card_number)
|
17
|
+
attachments = find(:all, :params => { :card_number => card_number })
|
18
|
+
attachments.each do |attachment|
|
19
|
+
url = self.site + attachment.url
|
20
|
+
url.userinfo = nil, nil
|
21
|
+
puts "Downloading #{url.to_s}:"
|
22
|
+
command = "curl --insecure --progress-bar --output #{attachment.file_name} --user #{username}:#{password} #{url}"
|
23
|
+
curl(command)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.attach(card_number, file_name, username, password)
|
29
|
+
if card_to_update = Card.find(card_number)
|
30
|
+
url = (site.to_s.gsub(/#{site.path}$/, '')) + collection_path(:card_number => card_number)
|
31
|
+
if File.exist?(file_name)
|
32
|
+
File.open(file_name) do |file|
|
33
|
+
body = { 'file' => file, "filename" => file_name }
|
34
|
+
client = HTTPClient.new
|
35
|
+
client.set_auth(nil, username, password)
|
36
|
+
response = client.post(url, body)
|
37
|
+
if response.status_code == 201
|
38
|
+
puts "File '#{file_name}' attached to card #{card_number}"
|
39
|
+
else
|
40
|
+
puts "Error attaching file '#{file_name}' to card #{card_number} (Got back HTTP code #{response.status_code})"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
else
|
44
|
+
warn "Unable to open file '#{file_name}'"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Resources
|
4
|
+
|
5
|
+
class Base < ActiveResource::Base
|
6
|
+
|
7
|
+
def self.configure(uri_options)
|
8
|
+
uri = URI.parse(uri_options[:url])
|
9
|
+
uri.user = uri_options[:username]
|
10
|
+
uri.password = uri_options[:password]
|
11
|
+
self.site = uri
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.print_collection(collection, attributes, align = :left)
|
15
|
+
output = []
|
16
|
+
longest_attributes = Array.new(attributes.length, 0)
|
17
|
+
alignment = (align == :left ? :ljust : :rjust)
|
18
|
+
collection.each do |element|
|
19
|
+
entry = []
|
20
|
+
attributes.each_with_index do |attribute, index|
|
21
|
+
attribute_value = element.send(attribute).to_s
|
22
|
+
longest_attributes[index] = attribute_value.length if attribute_value.length > longest_attributes[index]
|
23
|
+
entry << attribute_value
|
24
|
+
end
|
25
|
+
output << entry
|
26
|
+
end
|
27
|
+
output.each do |entry|
|
28
|
+
row = []
|
29
|
+
entry.each_with_index do |part, index|
|
30
|
+
row << [part.send(alignment, longest_attributes[index])]
|
31
|
+
end
|
32
|
+
puts row.join(" - ")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.warn(message)
|
37
|
+
puts "Warning: #{message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Resources
|
2
|
+
|
3
|
+
class Card < Base
|
4
|
+
|
5
|
+
def self.create(options = {}, status_property = nil)
|
6
|
+
options.merge!({status_property.to_sym => "New"}) if status_property
|
7
|
+
card = self.new(options)
|
8
|
+
if card.save
|
9
|
+
card.reload
|
10
|
+
puts "Card ##{card.number} created"
|
11
|
+
else
|
12
|
+
warn "Unable to create card"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.move(card_number, options = {}, config = {})
|
17
|
+
if card_to_move = find(card_number)
|
18
|
+
transition_options = { :card => card_number }
|
19
|
+
transition_options.merge!({ :comment => options[:comment]}) if options[:comment]
|
20
|
+
if config[:status_property]
|
21
|
+
current_status = card_to_move.send(config[:status_property])
|
22
|
+
else
|
23
|
+
warn "No known status of card ##{card_number}, cannot move!"
|
24
|
+
return
|
25
|
+
end
|
26
|
+
next_transition = nil
|
27
|
+
|
28
|
+
card_type = card_to_move.card_type_name.downcase
|
29
|
+
case card_type
|
30
|
+
when /defect/
|
31
|
+
status_states = config.select do |key, value|
|
32
|
+
key.to_s =~ /^defect/
|
33
|
+
end
|
34
|
+
when /task/
|
35
|
+
status_states = config.select do |key, value|
|
36
|
+
key.to_s =~ /^task_state_/
|
37
|
+
end
|
38
|
+
when /story/
|
39
|
+
status_states = config.select do |key, value|
|
40
|
+
key.to_s =~ /^story_state/
|
41
|
+
end
|
42
|
+
else
|
43
|
+
warn "No transitions defined for card of type #{card_to_move.card_type_name}"
|
44
|
+
return
|
45
|
+
end
|
46
|
+
status_states = status_states.collect {|state| state.last }.collect {|state| state.split(">").collect { |value| value.strip } }
|
47
|
+
next_transition = status_states.select {|state| state.first.downcase == current_status.downcase }.first.last
|
48
|
+
transition_options.merge!({ :transition => next_transition })
|
49
|
+
|
50
|
+
if response = TransitionExecution.create(transition_options)
|
51
|
+
if response.attributes["status"] == "completed"
|
52
|
+
puts "Moved card from #{current_status} to #{next_transition}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
else
|
56
|
+
warn "No card ##{card_number} found to move"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.print_all(options = [], status_property = nil)
|
61
|
+
attributes = [:number, :card_type_name, status_property, :name].compact
|
62
|
+
cards = find(:all)
|
63
|
+
cards.send(:extend, Minglr::Extensions::Array)
|
64
|
+
cards = cards.filter(attributes, options)
|
65
|
+
if cards.any?
|
66
|
+
print_collection(cards, attributes)
|
67
|
+
else
|
68
|
+
warn "No cards found"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.print_card(card_number, status_property = nil)
|
73
|
+
if card = find(card_number.to_i)
|
74
|
+
puts card.to_s(status_property)
|
75
|
+
else
|
76
|
+
warn "No card ##{card_number} found"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.update(card_number, options = {})
|
81
|
+
if card_to_update = find(card_number)
|
82
|
+
options.each do |attribute, value|
|
83
|
+
card_to_update.send("#{attribute.to_s}=".to_sym, value)
|
84
|
+
end
|
85
|
+
card_to_update.save
|
86
|
+
puts "Card ##{card_to_update.number} updated\n\n"
|
87
|
+
puts card_to_update.to_s
|
88
|
+
else
|
89
|
+
warn "Unable to update card ##{card_number}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s(status_property = nil)
|
94
|
+
attachments = []
|
95
|
+
begin
|
96
|
+
attachments = Attachment.find(:all, :params => { :card_number => number })
|
97
|
+
attachments = attachments.collect do |attachment|
|
98
|
+
"* #{attachment.file_name}: #{Resources::Base.site + attachment.url}"
|
99
|
+
end
|
100
|
+
rescue ActiveResource::ResourceNotFound => error
|
101
|
+
attachments = ["N/A"]
|
102
|
+
end
|
103
|
+
output = <<-EOS
|
104
|
+
Number: #{number}
|
105
|
+
Name: #{name}
|
106
|
+
Type: #{card_type_name.nil? ? "N/A" : card_type_name}
|
107
|
+
Status: #{send(status_property) if status_property && self.respond_to?(status_property)}
|
108
|
+
Description: #{description.nil? ? "N/A" : description}
|
109
|
+
|
110
|
+
Attachments:
|
111
|
+
#{attachments.join("\n")}
|
112
|
+
EOS
|
113
|
+
output
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|