Tracker 0.1.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +45 -18
- data/Rakefile +52 -0
- data/bin/trac +69 -52
- data/lib/comment.rb +7 -0
- data/lib/environment.rb +31 -0
- data/lib/ticket.rb +38 -0
- data/lib/tracker.rb +127 -0
- metadata +22 -53
- data/MIT-LICENSE +0 -9
- data/db/migrate +0 -70
- data/lib/action.rb +0 -220
- data/lib/bug.rb +0 -63
- data/lib/bug_description.rb +0 -15
- data/lib/database.rb +0 -44
- data/lib/helper.rb +0 -16
- data/lib/interface.rb +0 -175
- data/lib/prompter.rb +0 -66
- data/test/test_action.rb +0 -147
- data/test/test_bug.rb +0 -67
- data/test/test_bug_description.rb +0 -33
- data/test/test_integration.rb +0 -0
- data/test/test_interface.rb +0 -17
data/lib/bug_description.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require_gem 'activerecord'
|
3
|
-
|
4
|
-
# Fields are bug_id (int), date (date), description (text).
|
5
|
-
class BugDescription < ActiveRecord::Base
|
6
|
-
belongs_to :bug
|
7
|
-
before_save :save_date_time
|
8
|
-
before_update :save_date_time
|
9
|
-
|
10
|
-
private
|
11
|
-
# Saves date_time as current time
|
12
|
-
def save_date_time
|
13
|
-
self.date_time = Time.now.to_s(:db)
|
14
|
-
end
|
15
|
-
end
|
data/lib/database.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require_gem "activerecord"
|
3
|
-
|
4
|
-
# This module handles the connection to a database. There are only two methods, one to connect, one to create (connect_to_database and create_database, repsectively).
|
5
|
-
module Database
|
6
|
-
|
7
|
-
# This method should be called when starting the script to connect the program
|
8
|
-
# to the database.
|
9
|
-
def self.connect_to_database(database)
|
10
|
-
# Start the script by connecting to the sqlite3 datbase
|
11
|
-
ActiveRecord::Base.establish_connection(
|
12
|
-
:adapter => "sqlite3",
|
13
|
-
:database => database
|
14
|
-
)
|
15
|
-
end
|
16
|
-
|
17
|
-
# If no database currently exists, this method will create one using
|
18
|
-
# ActiveRecord's migrations
|
19
|
-
def self.create_database(database)
|
20
|
-
puts "Creating database '#{database}' in current directory..."
|
21
|
-
connect_to_database(database)
|
22
|
-
|
23
|
-
# Creates the bugs table
|
24
|
-
ActiveRecord::Schema.define do
|
25
|
-
create_table "bugs" do |t|
|
26
|
-
t.column "name", :string
|
27
|
-
t.column "priority", :int
|
28
|
-
t.column "area", :string
|
29
|
-
t.column "status", :boolean, :default => false
|
30
|
-
t.column "date_time", :datetime
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
# Creates the bug_descriptions
|
35
|
-
ActiveRecord::Schema.define do
|
36
|
-
create_table "bug_descriptions" do |t|
|
37
|
-
t.column "bug_id", :int
|
38
|
-
t.column "date_time", :datetime
|
39
|
-
t.column "description", :text
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
data/lib/helper.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
# This module includes static methods that do not fall into cateogries. These are just helper methods for various algorithms throughout the source code.
|
2
|
-
module Helper
|
3
|
-
|
4
|
-
# Takes a hash and returns the conditions sql for an ActiveRecord find method
|
5
|
-
def self.hash_to_sql(hash)
|
6
|
-
conditions = Array.new
|
7
|
-
hash.each do |attribute, term|
|
8
|
-
conditions << "#{attribute} = ?"
|
9
|
-
end
|
10
|
-
sql_string = conditions.join(" and ")
|
11
|
-
sql = [sql_string]
|
12
|
-
hash.each { |attribute, term| sql << term }
|
13
|
-
return sql
|
14
|
-
end
|
15
|
-
|
16
|
-
end
|
data/lib/interface.rb
DELETED
@@ -1,175 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + "/prompter.rb"
|
2
|
-
require File.dirname(__FILE__) + "/database.rb"
|
3
|
-
require File.dirname(__FILE__) + "/action.rb"
|
4
|
-
require File.dirname(__FILE__) + "/bug.rb"
|
5
|
-
require File.dirname(__FILE__) + "/bug_description.rb"
|
6
|
-
require 'yaml'
|
7
|
-
|
8
|
-
# The Interface's job is to act as the interface between the user and the Action module. The Interface will make sure that messages are outputted to the user and pass along actions to the Action module depending on whatever the command is.
|
9
|
-
class Interface
|
10
|
-
|
11
|
-
# Let the tracker interface have access to the bugger - controller
|
12
|
-
def initialize()
|
13
|
-
@filename = "trac"
|
14
|
-
config = {}
|
15
|
-
if File.exist?(ENV['HOME'] + "/.tracrc")
|
16
|
-
config = open(ENV['HOME'] + '/.tracrc') { |f| YAML.load(f) }
|
17
|
-
end
|
18
|
-
@all_options = ["name", "priority", "area", "description"].delete_if { |i| config.has_key? i }
|
19
|
-
end
|
20
|
-
|
21
|
-
# Parses ARGV and executes the correct bugger operations
|
22
|
-
# Will print out to the user whatever the user requested
|
23
|
-
def parse_command(command_line_arguments)
|
24
|
-
# Parse the command
|
25
|
-
hash = args_to_hash(command_line_arguments)
|
26
|
-
command = command_line_arguments[0]
|
27
|
-
if command_line_arguments.size > 1 && command_line_arguments[1][0,1] != "-"
|
28
|
-
get_name_or_id(command_line_arguments[1], hash)
|
29
|
-
end
|
30
|
-
|
31
|
-
# Perform the correct action for whatever command it is
|
32
|
-
if command.include?("append") # Appends a description to existing bug
|
33
|
-
hash = include_name_or_id(hash)
|
34
|
-
hash[:description] = Prompter.description? if !hash.has_key?(:description)
|
35
|
-
Action.append_description(hash)
|
36
|
-
elsif command.include?("areas")
|
37
|
-
puts "Searched ticket areas are:"
|
38
|
-
Action.list_areas(hash).each { |area| puts " * " + area }
|
39
|
-
elsif command.include?("names")
|
40
|
-
puts "Searched ticket names are:"
|
41
|
-
Action.list_names(hash).each { |name| puts " * " + name }
|
42
|
-
elsif command.include?("close") # Closes an existing bug
|
43
|
-
hash = include_name_or_id(hash)
|
44
|
-
Action.close_bug(hash)
|
45
|
-
elsif command.include?("delete") # Deletes the bug from the database
|
46
|
-
hash = include_name_or_id(hash)
|
47
|
-
Action.delete_bug(hash)
|
48
|
-
elsif command.include?("list") # Lists either all open bugs or bugs by conditions
|
49
|
-
bugs = Action.list_bugs(hash)
|
50
|
-
puts bugs.collect { |bug| get_print_info(bug) }.join("\n") if bugs.size > 0
|
51
|
-
elsif command.include?("new") # Creates a new bug
|
52
|
-
set_options(@all_options, hash)
|
53
|
-
Action.new_bug(hash)
|
54
|
-
elsif command.include?("open") # Opens a closed bug
|
55
|
-
hash = include_name_or_id(hash)
|
56
|
-
Action.open_bug(hash)
|
57
|
-
elsif command.include?("update")
|
58
|
-
hash = include_name_or_id(hash)
|
59
|
-
Action.update_bug(hash)
|
60
|
-
elsif command.include?("help") || command.include?("--help") # Prints usage
|
61
|
-
if !hash.has_key?(:name)
|
62
|
-
print_usage
|
63
|
-
elsif
|
64
|
-
method_name = "help_" + hash[:name]
|
65
|
-
if Action.respond_to?(method_name)
|
66
|
-
Action.send(method_name)
|
67
|
-
else
|
68
|
-
puts "The command #{hash[:name]} doesn't exist - try 'trac help' for a list of commands."
|
69
|
-
end
|
70
|
-
end
|
71
|
-
else
|
72
|
-
puts "Try '#{@filename} help'."
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
|
77
|
-
private
|
78
|
-
# Changes ARGV into a hash, ie. trac --name NAME --priority 2 becomes {:name => "NAME", :priority => 2}
|
79
|
-
def args_to_hash(args)
|
80
|
-
hash = Hash.new
|
81
|
-
hash[:id] = args[args.index("--id") + 1].to_i if args.include?("--id")
|
82
|
-
hash[:id] = args[args.index("-i") + 1].to_i if args.include?("-i")
|
83
|
-
hash[:name] = args[args.index("--name") + 1] if args.include?("--name")
|
84
|
-
hash[:name] = args[args.index("-n") + 1] if args.include?("-n")
|
85
|
-
hash[:priority] = args[args.index("--priority") + 1].to_i if args.include?("--priority")
|
86
|
-
hash[:priority] = args[args.index("-p") + 1].to_i if args.include?("-p")
|
87
|
-
hash[:area] = args[args.index("--area") + 1] if args.include?("--area")
|
88
|
-
hash[:area] = args[args.index("-a") + 1] if args.include?("-a")
|
89
|
-
hash[:description] = args[args.index("--description") + 1] if args.include?("--description")
|
90
|
-
hash[:description] = args[args.index("-d") + 1] if args.include?("-d")
|
91
|
-
hash[:status] = false if args.include?("--open") or args.include?("-o")
|
92
|
-
hash[:status] = true if args.include?("--closed") or args.include?("-c")
|
93
|
-
return hash
|
94
|
-
end
|
95
|
-
|
96
|
-
# Inspects the hash 'given' to see if it has the 'needed' keys. If it doesn't,
|
97
|
-
# will prompt the user for items.
|
98
|
-
def set_options(needed, given)
|
99
|
-
needed.each do |item|
|
100
|
-
if !given.has_key?(item.to_sym)
|
101
|
-
given[item.to_sym] = Prompter.send(item+"?")
|
102
|
-
end
|
103
|
-
end
|
104
|
-
return given
|
105
|
-
end
|
106
|
-
|
107
|
-
# If the given hash doesn't include an :id or :name key, prompts the user for
|
108
|
-
# an :id key and inserts it into the hash
|
109
|
-
def include_name_or_id(hash)
|
110
|
-
return hash if hash.has_key?(:name) or hash.has_key?(:id)
|
111
|
-
hash[:id] = Prompter.id?
|
112
|
-
return hash
|
113
|
-
end
|
114
|
-
|
115
|
-
# Determines if a given argument is an id or name and includes it in a hash as
|
116
|
-
# long as no other id or name was already included in the hash
|
117
|
-
def get_name_or_id(name_or_id, hash = Hash.new)
|
118
|
-
hash[:id] = name_or_id.to_i if name_or_id.to_i.kind_of?(Fixnum) && !hash.has_key?(:name) && name_or_id.to_i != 0
|
119
|
-
hash[:name] = name_or_id if name_or_id.kind_of?(String) && !hash.has_key?(:id)
|
120
|
-
return hash
|
121
|
-
end
|
122
|
-
|
123
|
-
# Returns the info for a bug to be printed out to the user
|
124
|
-
def get_print_info(bug)
|
125
|
-
working_string = String.new
|
126
|
-
working_string << "Ticket(#{bug.id}): #{bug.name}\n" if bug.id < 10
|
127
|
-
working_string << "Ticket(#{bug.id}): #{bug.name}\n" if bug.id >= 10 && bug.id < 100
|
128
|
-
working_string << "Ticket(#{bug.id}): #{bug.name}\n" if bug.id >= 100
|
129
|
-
working_string << "Priority: #{bug.priority}\n" unless bug.priority.nil?
|
130
|
-
working_string << "Area: #{bug.area}\n" unless bug.area.nil?
|
131
|
-
working_string << "Description - #{bug.date_time.to_s(:short)}:\n"
|
132
|
-
descriptions = bug.bug_descriptions.collect { |desc| " * " + desc.description }.join
|
133
|
-
working_string << descriptions
|
134
|
-
return working_string
|
135
|
-
end
|
136
|
-
|
137
|
-
# Prints a description out
|
138
|
-
def print_description(description)
|
139
|
-
puts description.description # The description method
|
140
|
-
puts "-"
|
141
|
-
end
|
142
|
-
|
143
|
-
# Tracker's usage
|
144
|
-
def print_usage
|
145
|
-
puts "usage: #{@filename} <command> [options] [-f DIRECTORY | DATABASE]"
|
146
|
-
puts ""
|
147
|
-
puts "Available commands:"
|
148
|
-
puts " create"
|
149
|
-
puts " append [-i ID | -n NAME | -d DESCRIPTION]"
|
150
|
-
puts " close [-i ID | -n NAME]"
|
151
|
-
puts " delete [-i ID | -n NAME]"
|
152
|
-
puts " list [-i ID | -n NAME | -p PRIORITY | -a AREA | -c | -o]"
|
153
|
-
puts " help [command]"
|
154
|
-
puts " new [-n NAME | -p PRIORITY | -a AREA | -d DESCRIPTION]"
|
155
|
-
puts " open [-i ID | -n NAME]"
|
156
|
-
puts " update [-i ID | -n NAME | -p PRIORITY | -a AREA]"
|
157
|
-
puts " areas [-n NAME | -p PRIORITY | -c | -o]"
|
158
|
-
puts " names [-a AREA | -p PRIORITY | -c | -o]"
|
159
|
-
puts ""
|
160
|
-
puts "Available options:"
|
161
|
-
puts " -i ID | --id ID"
|
162
|
-
puts " -n NAME | --name NAME"
|
163
|
-
puts " -p PRIORITY | --priority PRIORITY"
|
164
|
-
puts " -a AREA | --area AREA"
|
165
|
-
puts " -d DESCRIPTION | --description DESCRIPTION"
|
166
|
-
puts " -c | --closed"
|
167
|
-
puts " -o | --open"
|
168
|
-
puts " -f FILE | --file FILE"
|
169
|
-
puts ""
|
170
|
-
puts "Tracker is a command-line client for a bugs/features database. The database is kept in a flat file '.tracker_db' in the current directory."
|
171
|
-
puts ""
|
172
|
-
puts "To create a '.tracker_db' file, try using '#{@filename} create'."
|
173
|
-
end
|
174
|
-
|
175
|
-
end
|
data/lib/prompter.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
# Prompts the user for information. All prompts are contained into this class. All methods are class methods.
|
2
|
-
class Prompter
|
3
|
-
|
4
|
-
# Asks for id
|
5
|
-
def self.id?
|
6
|
-
self.int("id")
|
7
|
-
end
|
8
|
-
|
9
|
-
# Asks for area
|
10
|
-
def self.area?
|
11
|
-
self.string("area")
|
12
|
-
end
|
13
|
-
|
14
|
-
# Ask for priority - int
|
15
|
-
def self.priority?
|
16
|
-
self.int("priority")
|
17
|
-
end
|
18
|
-
|
19
|
-
# Asks for a name
|
20
|
-
def self.name?
|
21
|
-
self.string("name")
|
22
|
-
end
|
23
|
-
|
24
|
-
# Asks for description. Terminate with ^d on empty line.
|
25
|
-
def self.description?
|
26
|
-
self.text("description")
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
# Prompt for a string
|
31
|
-
def self.string(term)
|
32
|
-
print term.capitalize + ": "
|
33
|
-
STDIN.gets.gsub("\n", "")
|
34
|
-
end
|
35
|
-
|
36
|
-
# Prompt for an int
|
37
|
-
def self.int(term)
|
38
|
-
print term.capitalize + ": "
|
39
|
-
STDIN.gets.to_i
|
40
|
-
end
|
41
|
-
|
42
|
-
# Prompt for text. Terminate with ^d on empty line.
|
43
|
-
def self.text(term)
|
44
|
-
# Create the method
|
45
|
-
puts term.capitalize + ":"
|
46
|
-
text = String.new
|
47
|
-
string = STDIN.gets
|
48
|
-
while string
|
49
|
-
text += string
|
50
|
-
string = STDIN.gets
|
51
|
-
end
|
52
|
-
return text
|
53
|
-
end
|
54
|
-
|
55
|
-
# Prompt for a yes or no.
|
56
|
-
def self.boolean(term)
|
57
|
-
print term.capitalize + ": "
|
58
|
-
string = STDIN.gets
|
59
|
-
if string.include?("Y") || string.include?("y")
|
60
|
-
return true
|
61
|
-
else
|
62
|
-
return false
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
end
|
data/test/test_action.rb
DELETED
@@ -1,147 +0,0 @@
|
|
1
|
-
require "test/unit"
|
2
|
-
|
3
|
-
require File.dirname(__FILE__) + "/../lib/action.rb"
|
4
|
-
require File.dirname(__FILE__) + "/../lib/database.rb"
|
5
|
-
require File.dirname(__FILE__) + "/../lib/bug_description.rb"
|
6
|
-
|
7
|
-
class TestAction < Test::Unit::TestCase
|
8
|
-
def setup
|
9
|
-
Database::connect_to_database(".tracker_db")
|
10
|
-
@bug_names = ["First", "Second", "Third", "Fourth"]
|
11
|
-
@bug_ids = [1, 2, 3, 4]
|
12
|
-
@bug_priorities = [1, 2, 3, 4]
|
13
|
-
@bug_areas = ["One", "Two", "Three", "Four"]
|
14
|
-
@bug_status = [true, true, false, false]
|
15
|
-
@descriptions =
|
16
|
-
["First description", "Second description", "Third description"]
|
17
|
-
end
|
18
|
-
|
19
|
-
def teardown
|
20
|
-
# Delete all bugs that aren't in the migration
|
21
|
-
bugs = Bug.find(:all)
|
22
|
-
bugs.delete_if { |bug| @bug_ids.include?(bug.id) }
|
23
|
-
bugs.each { |bug| bug.destroy }
|
24
|
-
end
|
25
|
-
|
26
|
-
def test_append_description
|
27
|
-
bug = Bug.create(:name => "test_append_description",
|
28
|
-
:priority => 1, :area => "none")
|
29
|
-
Action.append_description(:id => bug.id, :description => "Hello")
|
30
|
-
Action.append_description(:name => "test_append_description",
|
31
|
-
:description => "World")
|
32
|
-
assert_equal(bug.bug_descriptions[0].description, "Hello")
|
33
|
-
assert_equal(bug.bug_descriptions[1].description, "World")
|
34
|
-
end
|
35
|
-
|
36
|
-
# An empty description should NOT be appened.
|
37
|
-
def test_append_description_that_is_empty
|
38
|
-
bug = Bug.create(:name => "test_append_empty_description",
|
39
|
-
:priority => 1, :area => "none")
|
40
|
-
descriptions_to_test = ["", " ", " ", "\n", "\n\n", " \n ", "\n ", " \n"]
|
41
|
-
descriptions_to_test.each do |string|
|
42
|
-
Action.append_description(:name => "test_append_empty_description",
|
43
|
-
:description => string)
|
44
|
-
end
|
45
|
-
bug = Bug.find(bug.id)
|
46
|
-
assert_equal(0, bug.bug_descriptions.size)
|
47
|
-
end
|
48
|
-
|
49
|
-
def test_close_bug
|
50
|
-
bug = Bug.find_by_status(false)
|
51
|
-
Action.close_bug(:id => bug.id)
|
52
|
-
bug = Bug.find_by_id(bug.id)
|
53
|
-
assert(bug.closed?)
|
54
|
-
bug.status = false
|
55
|
-
bug.save
|
56
|
-
end
|
57
|
-
|
58
|
-
def test_delete_bug
|
59
|
-
bug = Bug.create(:name => "test_delete_bug",
|
60
|
-
:priority => 2, :area => "none")
|
61
|
-
Action.delete_bug(:name => "test_delete_bug")
|
62
|
-
assert(!Bug.find_by_name("test_delete_bug"))
|
63
|
-
end
|
64
|
-
|
65
|
-
def test_list_bugs
|
66
|
-
bugs = Action.list_bugs
|
67
|
-
bugs.each { |bug| assert(@bug_names.include?(bug.name)) }
|
68
|
-
bugs = Action.list_bugs(:name => "First")
|
69
|
-
assert_equal(0, bugs.size)
|
70
|
-
bugs = Action.list_bugs(:status => false)
|
71
|
-
assert_equal(2, bugs.size)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Make sure the listed bugs with a condition are ALL opened
|
75
|
-
def test_open_list_bugs
|
76
|
-
first = Bug.create(:name => "Another open bug", :area => "Four")
|
77
|
-
second = Bug.create(:name => "Another closed bug",
|
78
|
-
:area => "Four", :status => true)
|
79
|
-
bugs = Action.list_bugs(:area => "Four")
|
80
|
-
bugs.each { |bug| assert(bug.open?) }
|
81
|
-
end
|
82
|
-
|
83
|
-
def test_new_bug
|
84
|
-
bug = Action.new_bug(:name => "test_new_bug",
|
85
|
-
:priority => 1, :area => "none",
|
86
|
-
:description => "This is a test description")
|
87
|
-
assert(bug)
|
88
|
-
assert(Bug.find_by_name("test_new_bug"))
|
89
|
-
assert_equal(Bug.find_by_name("test_new_bug").name, "test_new_bug")
|
90
|
-
assert_equal(bug.bug_descriptions[0].description,
|
91
|
-
"This is a test description")
|
92
|
-
end
|
93
|
-
|
94
|
-
def test_open_bug
|
95
|
-
bug = Bug.find_by_status(true)
|
96
|
-
Action.open_bug(:id => bug.id)
|
97
|
-
bug = Bug.find_by_id(bug.id)
|
98
|
-
assert(bug.open?)
|
99
|
-
bug.status = true
|
100
|
-
bug.save
|
101
|
-
end
|
102
|
-
|
103
|
-
def test_update_bug
|
104
|
-
bug = Bug.create(:name => "test_update_bug",
|
105
|
-
:priority => 1, :area => "none")
|
106
|
-
assert(bug)
|
107
|
-
Action.update_bug(:id => bug.id, :name => "new_name",
|
108
|
-
:priority => 2, :area => "Nothing")
|
109
|
-
bug = Bug.find_by_id(bug.id)
|
110
|
-
assert_equal("new_name", bug.name)
|
111
|
-
assert_equal("Nothing", bug.area)
|
112
|
-
assert_equal(2, bug.priority)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Should list all _open_ areas
|
116
|
-
def test_list_areas
|
117
|
-
areas = Action.list_areas
|
118
|
-
known_areas = ["Four", "Three"]
|
119
|
-
areas.each { |area| assert(known_areas.include?(area)) }
|
120
|
-
end
|
121
|
-
|
122
|
-
# Should list all bug areas with the condition given
|
123
|
-
def test_list_areas_with_conditions
|
124
|
-
areas = Action.list_areas(:name => "Fourth")
|
125
|
-
assert_equal("Four", areas[0])
|
126
|
-
areas = Action.list_areas(:priority => 3)
|
127
|
-
assert_equal("Three", areas[0])
|
128
|
-
end
|
129
|
-
|
130
|
-
# Should list all _open_ bug names
|
131
|
-
def test_list_names
|
132
|
-
names = Action.list_names
|
133
|
-
known_names = ["Fourth", "Third"]
|
134
|
-
names.each { |name| assert(known_names.include?(name)) }
|
135
|
-
end
|
136
|
-
|
137
|
-
# Should list all _open_ bug names with the conditions given
|
138
|
-
def test_list_names_with_conditions
|
139
|
-
names = Action.list_names(:name => "Third")
|
140
|
-
assert_equal("Third", names[0])
|
141
|
-
names = Action.list_names(:priority => 2)
|
142
|
-
assert_equal(nil, names[0])
|
143
|
-
names = Action.list_names(:priority => 2, :status => true)
|
144
|
-
assert_equal("Second", names[0])
|
145
|
-
end
|
146
|
-
|
147
|
-
end
|