schubert-minglr 1.2.0 → 1.3.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/.gitignore +2 -0
- data/Rakefile +16 -3
- data/VERSION.yml +2 -2
- data/bin/mtx +3 -3
- 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 +6 -2
- data/lib/minglr/action.rb +2 -2
- data/lib/minglr/mtx/input_cache.rb +24 -0
- data/lib/minglr/mtx/options_parser.rb +58 -0
- data/lib/minglr/options_parser.rb +13 -10
- data/lib/minglr/resources/attachment.rb +6 -1
- data/lib/minglr/resources/card.rb +26 -16
- data/lib/minglr/resources/user.rb +0 -6
- data/minglr.gemspec +31 -9
- data/tasks/commit.sample.rake +3 -3
- data/test/action_test.rb +75 -0
- data/test/commands_test.rb +111 -0
- data/test/config_parser_test.rb +113 -0
- data/test/options_parser_test.rb +74 -0
- data/test/resources/attachment_test.rb +53 -3
- data/test/resources/base_test.rb +15 -2
- data/test/resources/card_test.rb +119 -9
- data/test/resources/project_test.rb +18 -0
- data/test/resources/user_test.rb +0 -7
- data/test/test_helper.rb +2 -0
- metadata +21 -7
- data/lib/minglr/input_cache.rb +0 -22
- data/lib/minglr/mtx_options_parser.rb +0 -53
data/.gitignore
CHANGED
data/Rakefile
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
3
|
|
4
|
+
task :default => [:rcov, :features]
|
5
|
+
|
4
6
|
begin
|
5
7
|
require 'jeweler'
|
6
8
|
Jeweler::Tasks.new do |gem|
|
@@ -36,6 +38,7 @@ begin
|
|
36
38
|
require 'rcov/rcovtask'
|
37
39
|
Rcov::RcovTask.new do |test|
|
38
40
|
test.libs << 'test'
|
41
|
+
test.rcov_opts = ['--exclude', 'gems', "--text-report", "--only-uncovered"]
|
39
42
|
test.pattern = 'test/**/*_test.rb'
|
40
43
|
test.verbose = true
|
41
44
|
end
|
@@ -46,8 +49,6 @@ rescue LoadError
|
|
46
49
|
end
|
47
50
|
|
48
51
|
|
49
|
-
task :default => :test
|
50
|
-
|
51
52
|
require 'rake/rdoctask'
|
52
53
|
Rake::RDocTask.new do |rdoc|
|
53
54
|
if File.exist?('VERSION.yml')
|
@@ -63,5 +64,17 @@ Rake::RDocTask.new do |rdoc|
|
|
63
64
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
64
65
|
end
|
65
66
|
|
67
|
+
begin
|
68
|
+
require 'cucumber'
|
69
|
+
require 'cucumber/rake/task'
|
70
|
+
|
71
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
72
|
+
t.cucumber_opts = "features --format pretty"
|
73
|
+
end
|
74
|
+
rescue LoadError
|
75
|
+
task :features do
|
76
|
+
abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
|
77
|
+
end
|
78
|
+
end
|
66
79
|
|
67
|
-
task "
|
80
|
+
task "ci" => ["rcov", "features", "gemspec", "build", "install"]
|
data/VERSION.yml
CHANGED
data/bin/mtx
CHANGED
@@ -2,12 +2,12 @@
|
|
2
2
|
|
3
3
|
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'minglr'))
|
4
4
|
|
5
|
-
uri_options, execution_options =
|
5
|
+
uri_options, execution_options = MTX::OptionsParser.parse(ARGV, :card, :transition)
|
6
6
|
|
7
7
|
Resources::Base.configure uri_options
|
8
8
|
|
9
|
-
if card = Card.find(execution_options[:card])
|
10
|
-
execution = TransitionExecution.create(execution_options)
|
9
|
+
if card = Resources::Card.find(execution_options[:card])
|
10
|
+
execution = Resources::TransitionExecution.create(execution_options)
|
11
11
|
raise "Transition aborted. Errors: #{execution.errors.full_messages}" if execution.errors.any?
|
12
12
|
else
|
13
13
|
raise "Card '#{execution_options[:card]}' cannot be found."
|
data/cucumber.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Feature: Cards
|
2
|
+
|
3
|
+
Scenario: Print list of cards in the project
|
4
|
+
Given the project "xp"
|
5
|
+
When I issue the "cards" action
|
6
|
+
Then the result should have "119 - Task - Build PDF Export Widget on UI" in it
|
7
|
+
And the result should have "105 - Epic - Find people, projects and documents" in it
|
8
|
+
|
9
|
+
Scenario: Print list of cards in the project with a filter
|
10
|
+
Given the project "xp"
|
11
|
+
And a keyword of "Project"
|
12
|
+
When I issue the "cards" action
|
13
|
+
Then the result should not have "119 - Task - Build PDF Export Widget on UI" in it
|
14
|
+
Then the result should have "113 - Feature - Update Project" in it
|
15
|
+
And the result should have "112 - Feature - Create Project" in it
|
16
|
+
And the result should have "105 - Epic - Find people, projects and documents" in it
|
17
|
+
|
18
|
+
Scenario: Print the details of a card in the project
|
19
|
+
Given the project "xp"
|
20
|
+
And the card number "112"
|
21
|
+
When I issue the "card" action
|
22
|
+
Then the result should have "Number: 112" in it
|
23
|
+
And the result should have "Name: Create Project" in it
|
24
|
+
And the result should have "Type: Feature" in it
|
25
|
+
And the result should have "Status:" in it
|
26
|
+
And the result should have "Description: N/A" in it
|
27
|
+
And the result should have "Attachments:" in it
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "shared_steps")
|
2
|
+
|
3
|
+
Given /^the project "([^\"]*)"$/ do |project|
|
4
|
+
@project = project
|
5
|
+
end
|
6
|
+
|
7
|
+
Given /^the card number "([^\"]*)"$/ do |card_number|
|
8
|
+
if @options
|
9
|
+
@options << [card_number]
|
10
|
+
else
|
11
|
+
@options = [card_number]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Given /^a keyword of "([^\"]*)"$/ do |keyword|
|
16
|
+
if @options
|
17
|
+
@options << [keyword]
|
18
|
+
else
|
19
|
+
@options = [keyword]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
When /^I issue the "([^\"]*)" action$/ do |action|
|
24
|
+
@action = action
|
25
|
+
@response = execute_minglr_command(@project, @action, @options)
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^the result should have "([^\"]*)" in it$/ do |string|
|
29
|
+
assert @response.split("\n").join(" ").include?(string), "Expected #{@response.inspect} to contain '#{string}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
Then /^the result should not have "([^\"]*)" in it$/ do |string|
|
33
|
+
assert !@response.split("\n").join(" ").include?(string), "Expected #{@response.inspect} to not contain '#{string}'"
|
34
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "test/unit/assertions"
|
2
|
+
require File.join(File.dirname(__FILE__), "..", "..", "lib", "minglr")
|
3
|
+
World(Test::Unit::Assertions)
|
4
|
+
|
5
|
+
module Kernel
|
6
|
+
|
7
|
+
def capture_stdout
|
8
|
+
$stdout = $cucumberout
|
9
|
+
yield
|
10
|
+
return $cucumberout
|
11
|
+
ensure
|
12
|
+
$stdout = STDOUT
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
def rc_config
|
18
|
+
{
|
19
|
+
:scrum => {
|
20
|
+
:url => "http://localhost:9090/projects/scrum",
|
21
|
+
:password => "mingle",
|
22
|
+
:status_property => "cp_status",
|
23
|
+
:username => "schubert"
|
24
|
+
},
|
25
|
+
:global => {
|
26
|
+
:default => "blank",
|
27
|
+
:username => "schubert"
|
28
|
+
},
|
29
|
+
:storytracker => {
|
30
|
+
:url => "http://localhost:9090/projects/storytracker",
|
31
|
+
:password => "mingle",
|
32
|
+
:status_property => "cp_status",
|
33
|
+
:username => "schubert"
|
34
|
+
},
|
35
|
+
:xp => {
|
36
|
+
:url => "http://localhost:9090/projects/xp",
|
37
|
+
:password => "mingle",
|
38
|
+
:username => "schubert"
|
39
|
+
},
|
40
|
+
:blank => {
|
41
|
+
:url => "http://localhost:9090/projects/blank",
|
42
|
+
:password => "mingle",
|
43
|
+
:username => "schubert"
|
44
|
+
},
|
45
|
+
:agilehybrid => {
|
46
|
+
:url => "http://localhost:9090/projects/agilehybrid",
|
47
|
+
:password => "mingle",
|
48
|
+
:status_property => "cp_status",
|
49
|
+
:username=>"schubert"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def execute_minglr_command(project, action, extra_arguments = [])
|
55
|
+
$cucumberout = StringIO.new
|
56
|
+
extra_arguments = [] if extra_arguments.nil?
|
57
|
+
uri_options = rc_config[:global] || {}
|
58
|
+
original_arguments = ([project, action] + extra_arguments)
|
59
|
+
|
60
|
+
uri_options.merge! rc_config[project.to_sym]
|
61
|
+
Resources::Base.configure uri_options
|
62
|
+
Resources::Attachment.configure
|
63
|
+
extra_options = Minglr::OptionsParser.parse(original_arguments)
|
64
|
+
|
65
|
+
output = capture_stdout do
|
66
|
+
Minglr::Action.execute(action, original_arguments, extra_options, rc_config[project.to_sym])
|
67
|
+
end
|
68
|
+
output.string.strip
|
69
|
+
end
|
data/lib/minglr.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
MINGLR_ENV = "normal" unless defined?(MINGLR_ENV)
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'activesupport'
|
3
5
|
require 'activeresource'
|
@@ -8,8 +10,10 @@ require File.join(prefix, "action")
|
|
8
10
|
require File.join(prefix, "options_parser")
|
9
11
|
require File.join(prefix, "config_parser")
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
+
mtx = File.join(prefix, "mtx", "*")
|
14
|
+
Dir[mtx].each do |file_name|
|
15
|
+
load file_name
|
16
|
+
end
|
13
17
|
|
14
18
|
require File.join(prefix, "resources", "base")
|
15
19
|
resources = File.join(prefix, "resources", "*")
|
data/lib/minglr/action.rb
CHANGED
@@ -12,7 +12,7 @@ module Minglr
|
|
12
12
|
begin
|
13
13
|
Commands.send(action, options, flag_options, config)
|
14
14
|
rescue ActiveResource::ResourceNotFound => error
|
15
|
-
puts error.message + " for URL #{Resources::Base.site}..."
|
15
|
+
puts error.message + " for URL '#{Resources::Base.site}' ..."
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -34,7 +34,7 @@ module Minglr
|
|
34
34
|
|
35
35
|
def self.card(options, flag_options, config)
|
36
36
|
card_number = options.first
|
37
|
-
Resources::Card.print_card(
|
37
|
+
Resources::Card.print_card(card_number, config[:status_property])
|
38
38
|
end
|
39
39
|
|
40
40
|
def self.cards(options, flag_options, config)
|
@@ -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
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
1
3
|
module Minglr
|
2
4
|
class OptionsParser
|
3
5
|
def self.parse(args, *required_by_command)
|
@@ -9,7 +11,7 @@ module Minglr
|
|
9
11
|
rescue ActiveResource::UnauthorizedAccess => exception
|
10
12
|
puts "Connection #{exception.message} to #{Resources::Base.site.to_s}"
|
11
13
|
puts "Did you set 'basic_authentication_enabled: true' in your auth_config.yml file?"
|
12
|
-
exit 1
|
14
|
+
exit 1 unless MINGLR_ENV == "test"
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
@@ -21,23 +23,23 @@ module Minglr
|
|
21
23
|
opts.separator "Valid Commands Are: #{Minglr::Action.valid_actions.join(", ")}"
|
22
24
|
|
23
25
|
opts.on("-n NAME", String, "Short name of card") do |card_name|
|
24
|
-
command_options[:name] = card_name
|
26
|
+
command_options[:name] = card_name.strip
|
25
27
|
end
|
26
28
|
|
27
29
|
opts.on("-d DESCRIPTION", String, "Description of card") do |card_description|
|
28
|
-
command_options[:description] = card_description
|
30
|
+
command_options[:description] = card_description.strip
|
29
31
|
end
|
30
32
|
|
31
33
|
opts.on("-t TYPE", String, "Type of card") do |card_type|
|
32
|
-
command_options[:card_type_name] = card_type
|
34
|
+
command_options[:card_type_name] = card_type.strip
|
33
35
|
end
|
34
36
|
|
35
37
|
opts.on("-c COMMENT", String, "Comment") do |comment|
|
36
|
-
command_options[:comment] = comment
|
38
|
+
command_options[:comment] = comment.strip
|
37
39
|
end
|
38
40
|
|
39
41
|
opts.on("-f FILE", String, "File to attach") do |file|
|
40
|
-
command_options[:file_attachment] = file
|
42
|
+
command_options[:file_attachment] = file.strip
|
41
43
|
end
|
42
44
|
|
43
45
|
unless project_options.empty?
|
@@ -56,17 +58,18 @@ module Minglr
|
|
56
58
|
|
57
59
|
opts.on_tail("-h", "--help", "Show this help message.") do |help|
|
58
60
|
puts opts
|
59
|
-
exit
|
61
|
+
exit 0 unless MINGLR_ENV == "test"
|
60
62
|
end
|
61
63
|
|
62
64
|
opts.on_tail("--version", "Show version") do
|
63
|
-
|
64
|
-
|
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"
|
65
68
|
end
|
66
69
|
|
67
70
|
if args.empty?
|
68
71
|
puts opts
|
69
|
-
exit
|
72
|
+
exit 0 unless MINGLR_ENV == "test"
|
70
73
|
end
|
71
74
|
end
|
72
75
|
|
@@ -8,6 +8,10 @@ module Resources
|
|
8
8
|
self.prefix += "cards/:card_number/"
|
9
9
|
end
|
10
10
|
|
11
|
+
def self.curl(command)
|
12
|
+
`#{command}`
|
13
|
+
end
|
14
|
+
|
11
15
|
def self.fetch(card_number, username, password)
|
12
16
|
if card_to_update = Card.find(card_number)
|
13
17
|
attachments = find(:all, :params => { :card_number => card_number })
|
@@ -15,7 +19,8 @@ module Resources
|
|
15
19
|
url = self.site + attachment.url
|
16
20
|
url.userinfo = nil, nil
|
17
21
|
puts "Downloading #{url.to_s}:"
|
18
|
-
|
22
|
+
command = "curl --insecure --progress-bar --output #{attachment.file_name} --user #{username}:#{password} #{url}"
|
23
|
+
curl(command)
|
19
24
|
end
|
20
25
|
end
|
21
26
|
end
|
@@ -7,7 +7,7 @@ module Resources
|
|
7
7
|
card = self.new(options)
|
8
8
|
if card.save
|
9
9
|
card.reload
|
10
|
-
puts "Card
|
10
|
+
puts "Card ##{card.number} created"
|
11
11
|
else
|
12
12
|
warn "Unable to create card"
|
13
13
|
end
|
@@ -17,7 +17,12 @@ module Resources
|
|
17
17
|
if card_to_move = find(card_number)
|
18
18
|
transition_options = { :card => card_number }
|
19
19
|
transition_options.merge!({ :comment => options[:comment]}) if options[:comment]
|
20
|
-
|
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
|
21
26
|
next_transition = nil
|
22
27
|
|
23
28
|
card_type = card_to_move.card_type_name.downcase
|
@@ -35,7 +40,8 @@ module Resources
|
|
35
40
|
key.to_s =~ /^story_state/
|
36
41
|
end
|
37
42
|
else
|
38
|
-
|
43
|
+
warn "No transitions defined for card of type #{card_to_move.card_type_name}"
|
44
|
+
return
|
39
45
|
end
|
40
46
|
status_states = status_states.collect {|state| state.last }.collect {|state| state.split(">").collect { |value| value.strip } }
|
41
47
|
next_transition = status_states.select {|state| state.first.downcase == current_status.downcase }.first.last
|
@@ -47,13 +53,13 @@ module Resources
|
|
47
53
|
end
|
48
54
|
end
|
49
55
|
else
|
50
|
-
warn "No card
|
56
|
+
warn "No card ##{card_number} found to move"
|
51
57
|
end
|
52
58
|
end
|
53
59
|
|
54
60
|
def self.print_all(options = [], status_property = nil)
|
55
61
|
attributes = [:number, :card_type_name, status_property, :name].compact
|
56
|
-
cards =
|
62
|
+
cards = find(:all)
|
57
63
|
cards.send(:extend, Minglr::Extensions::Array)
|
58
64
|
cards = cards.filter(attributes, options)
|
59
65
|
if cards.any?
|
@@ -64,8 +70,7 @@ module Resources
|
|
64
70
|
end
|
65
71
|
|
66
72
|
def self.print_card(card_number, status_property = nil)
|
67
|
-
|
68
|
-
if card = find(card_number)
|
73
|
+
if card = find(card_number.to_i)
|
69
74
|
puts card.to_s(status_property)
|
70
75
|
else
|
71
76
|
warn "No card ##{card_number} found"
|
@@ -78,24 +83,29 @@ module Resources
|
|
78
83
|
card_to_update.send("#{attribute.to_s}=".to_sym, value)
|
79
84
|
end
|
80
85
|
card_to_update.save
|
81
|
-
puts "Card
|
82
|
-
puts
|
86
|
+
puts "Card ##{card_to_update.number} updated\n\n"
|
87
|
+
puts card_to_update.to_s
|
83
88
|
else
|
84
|
-
warn "Unable to update card
|
89
|
+
warn "Unable to update card ##{card_number}"
|
85
90
|
end
|
86
91
|
end
|
87
92
|
|
88
93
|
def to_s(status_property = nil)
|
89
|
-
attachments =
|
90
|
-
|
91
|
-
|
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"]
|
92
102
|
end
|
93
103
|
output = <<-EOS
|
94
104
|
Number: #{number}
|
95
105
|
Name: #{name}
|
96
|
-
Type: #{card_type_name}
|
97
|
-
Status: #{send(status_property) if status_property}
|
98
|
-
Description: #{description}
|
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}
|
99
109
|
|
100
110
|
Attachments:
|
101
111
|
#{attachments.join("\n")}
|