jira_report 0.0.2 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +49 -17
- data/bin/jira-report +2 -4
- data/jira_report.gemspec +3 -1
- data/lib/jira_report.rb +1 -97
- data/lib/jira_report/cli.rb +118 -19
- data/lib/jira_report/config_loader.rb +21 -0
- data/lib/jira_report/options.rb +78 -0
- data/lib/jira_report/reporter.rb +134 -0
- data/lib/jira_report/version.rb +1 -1
- data/spec/jira_report/config_loader_spec.rb +43 -0
- data/spec/jira_report/options_spec.rb +86 -0
- data/spec/jira_report/reporter_spec.rb +181 -0
- metadata +15 -9
- data/lib/jira_report/settings.rb +0 -48
- data/spec/jira_report_spec.rb +0 -64
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a9197875b688ab8f04bca3835f25bc7edb62ac7
|
4
|
+
data.tar.gz: c048244ac02abeaad3fa7ff017d24bbef0869e4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa32b19bbf05ec4c43208314a4bc73b4beef7bf2edfaa8df2249f054aa089b4d4f4eac9659c3d2e507cb5423fcb5604e0489bb46afb82cae6cd715dfc54c0349
|
7
|
+
data.tar.gz: 192b673da211b18c7725ab369e0434b03533ddb5038a7890c2cd211da1d06664e289c19a2cd7eed69b678f8d9ea00c93e1e69451c61100f0c8a61f0ce79d26c6
|
data/README.md
CHANGED
@@ -1,27 +1,23 @@
|
|
1
|
-
[![Gem Version](https://badge.fury.io/rb/jira_report.svg)](https://rubygems.org/gems/jira_report)
|
2
|
-
[![Build Status](https://api.travis-ci.org/veelenga/jira_report.svg)](https://travis-ci.org/veelenga/jira_report)
|
3
|
-
|
4
|
-
Jira Report
|
1
|
+
Jira Report [![Gem Version](https://badge.fury.io/rb/jira_report.svg)](https://rubygems.org/gems/jira_report) [![Build Status](https://api.travis-ci.org/veelenga/jira_report.svg)](https://travis-ci.org/veelenga/jira_report)
|
5
2
|
===========================
|
6
3
|
|
7
|
-
Queries user activities for specified period of time and prints it to console.
|
4
|
+
Queries user activities from jira for specified period of time and prints it to console.
|
8
5
|
|
9
6
|
##Installation
|
10
7
|
```
|
11
|
-
$gem install jira_report
|
8
|
+
$ gem install jira_report
|
12
9
|
```
|
13
10
|
|
14
11
|
##Usage
|
15
|
-
```sh
|
16
|
-
$jira-report -h
|
17
|
-
Usage: jira-report [options]
|
18
|
-
-u, --username username Username to query activity report.
|
19
|
-
-c, --config config Path to config file. USER_HOME/.jira-report is default.
|
20
|
-
```
|
21
12
|
|
13
|
+
Just run it. `jira-report` will ask you your jira location, who you are and what's your password:
|
22
14
|
```
|
23
|
-
$jira-report
|
15
|
+
$ jira-report
|
16
|
+
Jira url: jira.company.com
|
17
|
+
Jira username: admin
|
18
|
+
Jira password:
|
24
19
|
Querying jira...
|
20
|
+
|
25
21
|
Jira activity report for [admin]:
|
26
22
|
|
27
23
|
Created: 2
|
@@ -41,19 +37,55 @@ Resolved: 8
|
|
41
37
|
Reopened: 0
|
42
38
|
|
43
39
|
Closed: 5
|
44
|
-
TST-6943 - Remove redundant org.apache.log4j
|
40
|
+
TST-6943 - Remove redundant org.apache.log4j dependency from common part
|
45
41
|
TST-5862 - Unable to install NGinx on HP-UX with Java 6
|
46
42
|
TST-5857 - Put back support for Jdk 1.6
|
47
43
|
TST-5840 - NGinx fails to handle interaction initiated
|
48
44
|
GSM-364 - Migration of existing units
|
49
45
|
```
|
50
46
|
|
47
|
+
`url`, `username`, `password` and some other parameters can be added to [configuration file](#configuration). Also you can use mixed approach (keep some options in file, others enter from command line). For example if you do not want to keep password in configuration file, just don't, you will be asked.
|
48
|
+
|
49
|
+
Also you can use it directly in ruby:
|
50
|
+
```ruby
|
51
|
+
require 'jira_report'
|
52
|
+
|
53
|
+
def print_issues(issues)
|
54
|
+
issues.each do |issue|
|
55
|
+
puts " #{issue['key']} - #{issue['fields']['summary']}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
reporter = JiraReport::Reporter.new('jira.company.com', 'admin', 's3cr3t')
|
60
|
+
# returns all created issues by 'my_jira_name'
|
61
|
+
all_created = reporter.created('my_jira_name')
|
62
|
+
|
63
|
+
# returns closed issues by 'admin' last week
|
64
|
+
weekly_closed = reporter.closed('admin', '-1w')
|
65
|
+
|
66
|
+
# returns reopened issues by 'usr' in period starting
|
67
|
+
# from two weeks ago and ending one week ago.
|
68
|
+
reopened = reporter.reopened('usr', '-2w', '-1w')
|
69
|
+
|
70
|
+
print_issues(weekly_closed)
|
71
|
+
```
|
72
|
+
|
51
73
|
##Configuration
|
52
|
-
|
74
|
+
Path to configuration file can be specified by `-c` command line argument. `~/.jira-report` is default.
|
53
75
|
|
54
|
-
|
76
|
+
There are three main options in config file to query jira:
|
77
|
+
```
|
78
|
+
url=jira.company.com
|
79
|
+
username=username
|
80
|
+
password=s3cr3t
|
81
|
+
```
|
82
|
+
all those are optional and if not specified user will be asked to enter it from command line.
|
55
83
|
|
84
|
+
Period is set by two options `period_from` and `period_till`. Both options support [advanced jira searching](https://confluence.atlassian.com/display/JIRA/Advanced+Searching) and accept dates, jira functions, aliasing. For example:
|
56
85
|
```
|
57
|
-
period_from=-
|
86
|
+
period_from=-1w
|
58
87
|
period_till=now()
|
59
88
|
```
|
89
|
+
If those options not specified in configuration file, last week activities will be queried (just as in example above).
|
90
|
+
|
91
|
+
You can look at configuration file [sample](example/jira-report.sample) in repository.
|
data/bin/jira-report
CHANGED
data/jira_report.gemspec
CHANGED
@@ -18,7 +18,9 @@ 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.
|
21
|
+
spec.required_ruby_version = '>= 1.9.3'
|
22
|
+
|
23
|
+
spec.add_runtime_dependency 'parseconfig', '~> 1.0'
|
22
24
|
spec.add_runtime_dependency 'rest-client', '~> 1.7'
|
23
25
|
spec.add_development_dependency 'bundler', '~> 1.7'
|
24
26
|
spec.add_development_dependency 'rake', '~> 10.0'
|
data/lib/jira_report.rb
CHANGED
@@ -1,97 +1 @@
|
|
1
|
-
require '
|
2
|
-
require 'json'
|
3
|
-
require 'uri'
|
4
|
-
|
5
|
-
require 'jira_report/settings'
|
6
|
-
require 'jira_report/cli'
|
7
|
-
|
8
|
-
module JiraReport
|
9
|
-
# Base class that generate jira activity report
|
10
|
-
class JiraReport
|
11
|
-
attr_accessor :from, :till, :username
|
12
|
-
|
13
|
-
def initialize(init_set, username)
|
14
|
-
@username = username ? username : init_set.username
|
15
|
-
@search_url = jira_search_url(init_set.url,
|
16
|
-
init_set.username,
|
17
|
-
init_set.password)
|
18
|
-
|
19
|
-
@from = init_set.period_from
|
20
|
-
@till = init_set.period_till
|
21
|
-
end
|
22
|
-
|
23
|
-
def report
|
24
|
-
puts "\nQuerying jira..."
|
25
|
-
|
26
|
-
begin
|
27
|
-
created = query(jql_created)
|
28
|
-
resolved = query(jql_resolved)
|
29
|
-
reopened = query(jql_reopened)
|
30
|
-
closed = query(jql_closed)
|
31
|
-
|
32
|
-
puts "\nJira activity report for [#{@username}]:"
|
33
|
-
|
34
|
-
puts "\nCreated: #{created.length}"
|
35
|
-
print_issues(created)
|
36
|
-
|
37
|
-
puts "\nResolved: #{resolved.length}"
|
38
|
-
print_issues(resolved)
|
39
|
-
|
40
|
-
puts "\nReopened: #{reopened.length}"
|
41
|
-
print_issues(reopened)
|
42
|
-
|
43
|
-
puts "\nClosed: #{closed.length}"
|
44
|
-
print_issues(closed)
|
45
|
-
|
46
|
-
rescue SocketError => e
|
47
|
-
puts "Unable to connect to jira: '#{e.message}'"
|
48
|
-
rescue Exception => e
|
49
|
-
puts "Unable to prepare activity report: '#{e.message}'"
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def jira_search_url(url, username, password)
|
56
|
-
"http://#{username}:#{password}@#{url}/rest/api/2/search?"
|
57
|
-
end
|
58
|
-
|
59
|
-
def jql_created
|
60
|
-
"jql=created>=#{@from} " \
|
61
|
-
"AND created<=#{@till} "\
|
62
|
-
"AND reporter=#{@username}"
|
63
|
-
end
|
64
|
-
|
65
|
-
def jql_resolved
|
66
|
-
"jql=resolved>=#{@from} " \
|
67
|
-
"AND resolved<=#{@till} "\
|
68
|
-
"AND 'First Resolution User'=#{@username}"
|
69
|
-
end
|
70
|
-
|
71
|
-
def jql_closed
|
72
|
-
"jql='First Closed Date'>=#{@from} " \
|
73
|
-
"AND 'First Closed Date'<=#{@till} "\
|
74
|
-
"AND 'First Closed User'=#{@username}"
|
75
|
-
end
|
76
|
-
|
77
|
-
def jql_reopened
|
78
|
-
"jql='First Reopened Date'>=#{@from} " \
|
79
|
-
"AND 'First Reopened Date'<=#{@till} "\
|
80
|
-
"AND 'First Reopened User'=#{@username}"
|
81
|
-
end
|
82
|
-
|
83
|
-
def query(jql)
|
84
|
-
response = RestClient.get(@search_url + URI.escape(jql))
|
85
|
-
unless response.code == 200
|
86
|
-
fail "Got wrong response code: #{response.code}"
|
87
|
-
end
|
88
|
-
JSON.parse(response.body)['issues']
|
89
|
-
end
|
90
|
-
|
91
|
-
def print_issues(issues)
|
92
|
-
issues.each do |issue|
|
93
|
-
puts " #{issue['key']} - #{issue['fields']['summary']}"
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
1
|
+
require 'jira_report/reporter'
|
data/lib/jira_report/cli.rb
CHANGED
@@ -1,31 +1,130 @@
|
|
1
|
-
require '
|
1
|
+
require 'jira_report/options'
|
2
|
+
require 'jira_report/config_loader'
|
3
|
+
require 'jira_report/reporter'
|
2
4
|
|
3
5
|
module JiraReport
|
4
|
-
# Prepares command line parameters using OptionParser
|
5
6
|
class Cli
|
6
|
-
|
7
|
+
DEFAULT_CONFIG_PATH = '~/.jira-report'
|
7
8
|
|
8
9
|
def initialize
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
@config = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(args=ARGV)
|
14
|
+
begin
|
15
|
+
options = Options.new(args).options
|
16
|
+
act_on_option(options)
|
17
|
+
|
18
|
+
load_config(options[:config])
|
19
|
+
ask_missing_options
|
20
|
+
add_usr(options[:username])
|
21
|
+
|
22
|
+
report
|
23
|
+
rescue => e
|
24
|
+
puts "error: '#{e.message}'"
|
25
|
+
exit 1
|
26
|
+
rescue SystemExit => e
|
27
|
+
exit e.status
|
28
|
+
rescue Interrupt => e
|
29
|
+
puts 'Interrupted'
|
30
|
+
exit 130
|
31
|
+
rescue Exception => e
|
32
|
+
puts "fatal: '#{e.message}'"
|
33
|
+
exit 255
|
34
|
+
end
|
12
35
|
end
|
13
36
|
|
14
37
|
private
|
15
38
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
39
|
+
def report
|
40
|
+
reporter = Reporter.new(
|
41
|
+
@config[:url], @config[:username], @config[:password]
|
42
|
+
)
|
43
|
+
|
44
|
+
puts "\nQuerying jira..."
|
45
|
+
|
46
|
+
created = reporter.created(
|
47
|
+
@config[:usr], @config[:period_from], @config[:period_till]
|
48
|
+
)
|
49
|
+
resolved = reporter.resolved(
|
50
|
+
@config[:usr], @config[:period_from], @config[:period_till]
|
51
|
+
)
|
52
|
+
reopened = reporter.reopened(
|
53
|
+
@config[:usr], @config[:period_from], @config[:period_till]
|
54
|
+
)
|
55
|
+
closed = reporter.closed(
|
56
|
+
@config[:usr], @config[:period_from], @config[:period_till]
|
57
|
+
)
|
58
|
+
|
59
|
+
puts "\nJira activity report for [#{@config[:usr]}]:"
|
60
|
+
|
61
|
+
puts "\nCreated: #{created.length}"
|
62
|
+
print_issues(created)
|
63
|
+
|
64
|
+
puts "\nResolved: #{resolved.length}"
|
65
|
+
print_issues(resolved)
|
66
|
+
|
67
|
+
puts "\nReopened: #{reopened.length}"
|
68
|
+
print_issues(reopened)
|
69
|
+
|
70
|
+
puts "\nClosed: #{closed.length}"
|
71
|
+
print_issues(closed)
|
72
|
+
end
|
73
|
+
|
74
|
+
def print_issues(issues)
|
75
|
+
issues.each do |issue|
|
76
|
+
puts " #{issue['key']} - #{issue['fields']['summary']}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Does required actions on specified option if needed.
|
81
|
+
def act_on_option(options)
|
82
|
+
if options.include? :version
|
83
|
+
puts VERSION
|
84
|
+
exit(0)
|
85
|
+
end
|
86
|
+
# ...
|
87
|
+
end
|
88
|
+
|
89
|
+
# Loads configuration from configuration file.
|
90
|
+
def load_config(path)
|
91
|
+
config_path = path ? path : DEFAULT_CONFIG_PATH
|
92
|
+
@config = ConfigLoader.load_config(config_path)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Reads required options from user input if those
|
96
|
+
# were missed.
|
97
|
+
def ask_missing_options
|
98
|
+
@config[:url] = ask('Jira url: ') unless @config[:url]
|
99
|
+
@config[:username] = ask('Jira username: ') unless @config[:username]
|
100
|
+
@config[:password] = ask('Jira password: '){
|
101
|
+
STDIN.noecho(&:gets).chomp!
|
102
|
+
} unless @config[:password]
|
103
|
+
if !@config[:period_from] && !@config[:period_till]
|
104
|
+
@config[:period_from] = '-1w'
|
105
|
+
@config[:period_till] = 'now()'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Asks user to enter something
|
110
|
+
def ask(message, &block)
|
111
|
+
print message
|
112
|
+
if block_given?
|
113
|
+
yield block
|
114
|
+
else
|
115
|
+
gets.chomp!
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Adds to config username to query about.
|
120
|
+
def add_usr(usr)
|
121
|
+
if usr
|
122
|
+
@config[:usr] = usr
|
123
|
+
else
|
124
|
+
# use username for authentication if username
|
125
|
+
# to query not specified.
|
126
|
+
@config[:usr] = @config[:username]
|
127
|
+
end
|
29
128
|
end
|
30
129
|
end
|
31
130
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'parseconfig'
|
2
|
+
|
3
|
+
module JiraReport
|
4
|
+
# Loads configuration from configuration file.
|
5
|
+
class ConfigLoader
|
6
|
+
def self.load_config(path)
|
7
|
+
begin
|
8
|
+
config = ParseConfig.new(File.expand_path(path))
|
9
|
+
symboled_hash(config.params)
|
10
|
+
rescue Exception => e
|
11
|
+
raise RuntimeError.new "Error loading config: #{e}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def self.symboled_hash(h)
|
18
|
+
h.inject({}){ |m, (k,v)| m[k.to_sym] = v; m }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module JiraReport
|
4
|
+
|
5
|
+
# Collection of input argument descriptions.
|
6
|
+
class OptionsText
|
7
|
+
TEXT = {
|
8
|
+
username: 'Specify jira username to query statistic about.',
|
9
|
+
config: 'Specify path to configuration file.',
|
10
|
+
version: 'Display version.'
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Parses and validates array of input arguments.
|
15
|
+
class Options
|
16
|
+
# Accepts arguments to be parsed.
|
17
|
+
# Raises ArgumentError if args is not kind of Array.
|
18
|
+
def initialize(args)
|
19
|
+
unless args.kind_of? Array
|
20
|
+
raise ArgumentError.new "Array expected, but was #{args.class}"
|
21
|
+
end
|
22
|
+
@options = {}
|
23
|
+
parse(args)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns clone of option hash
|
27
|
+
def options
|
28
|
+
@options.dup
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns option from option list with name that converted
|
32
|
+
# to symbol.
|
33
|
+
def get(sym)
|
34
|
+
@options[sym]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if option list includes option which name
|
38
|
+
# converted to symbol.
|
39
|
+
def include?(sym)
|
40
|
+
@options.include? sym
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Parses args. Adds parsed argument to option hash
|
46
|
+
# in next format:
|
47
|
+
# :option_name => option
|
48
|
+
# Where :option_name - corresponding symbol of
|
49
|
+
# option name.
|
50
|
+
def parse(args)
|
51
|
+
OptionParser.new do |opts|
|
52
|
+
opts.banner = 'Usage: jira-report [options]'
|
53
|
+
|
54
|
+
option(opts, '-u', '--username USERNAME')
|
55
|
+
option(opts, '-c', '--config FILE')
|
56
|
+
option(opts, '-v', '--version')
|
57
|
+
end.parse!(args)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Adds new option.
|
61
|
+
def option(opts, *args)
|
62
|
+
opt_sym = long_opt_sym(*args)
|
63
|
+
args << OptionsText::TEXT[opt_sym]
|
64
|
+
opts.on(*args) { |arg| @options[opt_sym] = arg }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Looks through arg list to find long option
|
68
|
+
# and converts it to sym. Assumes that long
|
69
|
+
# option always present.
|
70
|
+
#
|
71
|
+
# For example:
|
72
|
+
# ['-c', '--config-file FILE'] => :config_file
|
73
|
+
def long_opt_sym(*args)
|
74
|
+
long_opt = args.find{ |arg| arg.start_with? '--' }
|
75
|
+
long_opt[2..-1].sub(/ .*/, '').gsub('-', '_').to_sym
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module JiraReport
|
6
|
+
# With this class you can easily query user activities from jira
|
7
|
+
# in specified period of time.
|
8
|
+
#
|
9
|
+
# ==== Examples
|
10
|
+
#
|
11
|
+
# reporter = Reporter.new('jira.company.url', 'admin', 's3cr3t')
|
12
|
+
#
|
13
|
+
# # returns all created issues by 'my_jira_name'
|
14
|
+
# all_created = reporter.created('my_jira_name')
|
15
|
+
#
|
16
|
+
# # returns closed issues by 'admin' last week
|
17
|
+
# weekly_closed = reporter.closed('admin', '-1w')
|
18
|
+
#
|
19
|
+
# # returns reopened issues by 'usr' in period starting
|
20
|
+
# # from two weeks ago and ending one week ago.
|
21
|
+
# reopened = reporter.reopened('usr', '-2w', '-1w')
|
22
|
+
class Reporter
|
23
|
+
# Initializes reporter.
|
24
|
+
#
|
25
|
+
# ==== Attributes
|
26
|
+
#
|
27
|
+
# * +url+ - jira URL in `jira.company.com` format.
|
28
|
+
# * +usr+ - jira username.
|
29
|
+
# * +pass+ - jira password.
|
30
|
+
def initialize(url, usr, pass)
|
31
|
+
@search_url = jira_api_url(url, usr, pass);
|
32
|
+
end
|
33
|
+
|
34
|
+
# Queries created issues by usr in specified period.
|
35
|
+
#
|
36
|
+
# ==== Attributes
|
37
|
+
#
|
38
|
+
# * +usr+ - username query statistic about.
|
39
|
+
# * +from+ - period of time starts with this value (jql format).
|
40
|
+
# * +till+ - period of time ends with this value (jql format).
|
41
|
+
def created(usr, from=nil, till=nil)
|
42
|
+
query_issues(jql_created usr, from, till)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Queries resolved issues by usr in specified period.
|
46
|
+
#
|
47
|
+
# ==== Attributes
|
48
|
+
#
|
49
|
+
# * +usr+ - username query statistic about.
|
50
|
+
# * +from+ - period of time starts with this value (jql format).
|
51
|
+
# * +till+ - period of time ends with this value (jql format).
|
52
|
+
def resolved(usr, from=nil, till=nil)
|
53
|
+
query_issues(jql_resolved usr, from, till)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Queries reopened issues by usr in specified period.
|
57
|
+
#
|
58
|
+
# ==== Attributes
|
59
|
+
#
|
60
|
+
# * +usr+ - username query statistic about.
|
61
|
+
# * +from+ - period of time starts with this value (jql format).
|
62
|
+
# * +till+ - period of time ends with this value (jql format).
|
63
|
+
def reopened(usr, from=nil, till=nil)
|
64
|
+
query_issues(jql_reopened usr, from, till)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Queries closed issues by usr in specified period.
|
68
|
+
#
|
69
|
+
# ==== Attributes
|
70
|
+
#
|
71
|
+
# * +usr+ - username query statistic about.
|
72
|
+
# * +from+ - period of time starts with this value (jql format).
|
73
|
+
# * +till+ - period of time ends with this value (jql format).
|
74
|
+
def closed(usr, from=nil, till=nil)
|
75
|
+
query_issues(jql_closed usr, from, till)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Queries issues using jql query.
|
79
|
+
#
|
80
|
+
# ==== Attributes
|
81
|
+
#
|
82
|
+
# * +jql+ - query in jira query languages.
|
83
|
+
def query_issues(jql)
|
84
|
+
response = RestClient.get(@search_url + URI.escape(jql))
|
85
|
+
unless response.code == 200
|
86
|
+
fail "Response code: #{response.code}"
|
87
|
+
end
|
88
|
+
JSON.parse(response.body)['issues']
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Returns jira rest api search url.
|
94
|
+
def jira_api_url(url, username, password)
|
95
|
+
"http://#{username}:#{password}@#{url}/rest/api/2/search?"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Prepares jql query based on parameters to
|
99
|
+
# search created issues.
|
100
|
+
def jql_created(usr, from, till)
|
101
|
+
jql = 'jql='
|
102
|
+
jql << "created>=#{from} AND " if from
|
103
|
+
jql << "created<=#{till} AND " if till
|
104
|
+
jql << "reporter=#{usr}"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Prepares jql query based on parameters to
|
108
|
+
# search resolved issues.
|
109
|
+
def jql_resolved(usr, from, till)
|
110
|
+
jql = 'jql='
|
111
|
+
jql << "resolved>=#{from} AND " if from
|
112
|
+
jql << "resolved<=#{till} AND " if till
|
113
|
+
jql << "'First Resolution User'=#{usr}"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Prepares jql query based on parameters to
|
117
|
+
# search reopened issues.
|
118
|
+
def jql_reopened(usr, from, till)
|
119
|
+
jql = 'jql='
|
120
|
+
jql << "'First Reopened Date'>=#{from} AND " if from
|
121
|
+
jql << "'First Reopened Date'<=#{till} AND " if till
|
122
|
+
jql << "'First Reopened User'=#{usr}"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Prepares jql query based on parameters to
|
126
|
+
# search closed issues.
|
127
|
+
def jql_closed(usr, from, till)
|
128
|
+
jql = 'jql='
|
129
|
+
jql << "'First Closed Date'>=#{from} AND " if from
|
130
|
+
jql << "'First Closed Date'<=#{till} AND " if till
|
131
|
+
jql << "'First Closed User'=#{usr}"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/jira_report/version.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'jira_report/config_loader'
|
3
|
+
|
4
|
+
module JiraReport
|
5
|
+
describe ConfigLoader do
|
6
|
+
it 'raises error if wrong path to configuration file' do
|
7
|
+
expect{ ConfigLoader.load_config('wrong/path') }.to raise_error RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
context 'when file exists' do
|
11
|
+
let(:filename){ 'test.conf' }
|
12
|
+
let(:option){ 'username' }
|
13
|
+
let(:value){ 'JohnDoe' }
|
14
|
+
|
15
|
+
before do
|
16
|
+
FileUtils.touch(filename)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_content(filename, content)
|
20
|
+
f = File.new(filename, 'a+')
|
21
|
+
f.write(content)
|
22
|
+
f.flush
|
23
|
+
f.close
|
24
|
+
end
|
25
|
+
|
26
|
+
after do
|
27
|
+
FileUtils.rm_r(filename)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'reads empty file correctly' do
|
31
|
+
config = ConfigLoader.load_config(filename)
|
32
|
+
expect(config).not_to be nil
|
33
|
+
expect(config).to be_empty
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'read file with options' do
|
37
|
+
add_content(filename, "#{option}=#{value}")
|
38
|
+
config = ConfigLoader.load_config(filename)
|
39
|
+
expect(config).to include(option.to_sym => value)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'jira_report/options'
|
3
|
+
|
4
|
+
module JiraReport
|
5
|
+
describe Options do
|
6
|
+
def check_field(opts, s, v)
|
7
|
+
o = Options.new(opts)
|
8
|
+
expect(o.get(s)).to eq(v)
|
9
|
+
expect(o.options.include?(s)).to be true
|
10
|
+
expect(o.include?(s)).to be true
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_flag(opts, s)
|
14
|
+
o = Options.new(opts)
|
15
|
+
expect(o.include?(s)).to be true
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_field_err(opts, err=Exception)
|
19
|
+
expect{ Options.new(opts) }.to raise_error err
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when non-option passed' do
|
23
|
+
it 'should throw error if wrong option' do
|
24
|
+
check_field_err(['-1', 'value'], OptionParser::InvalidOption)
|
25
|
+
end
|
26
|
+
it 'should throw error if nil passed' do
|
27
|
+
check_field_err(nil, ArgumentError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when no arguments passed' do
|
32
|
+
let(:opt) { Options.new([]) }
|
33
|
+
|
34
|
+
it 'should have no options' do
|
35
|
+
expect(opt.options.empty?).to be true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '-h' do
|
40
|
+
before(:each) do
|
41
|
+
$stdout = StringIO.new
|
42
|
+
$stderr = StringIO.new
|
43
|
+
end
|
44
|
+
after(:each) do
|
45
|
+
$stdout = STDOUT
|
46
|
+
$stderr = STDERR
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'exits clearly' do
|
50
|
+
expect{ Options.new(['-h']) }.to raise_error SystemExit
|
51
|
+
expect{ Options.new(['--help']) }.to raise_error SystemExit
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '-v' do
|
56
|
+
it 'is accepted' do
|
57
|
+
check_flag(['-v'], :version)
|
58
|
+
check_flag(['--version'], :version)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '-u' do
|
63
|
+
it 'is accepted' do
|
64
|
+
usr = 'JohnDoe'
|
65
|
+
check_field(['-u', usr], :username, usr)
|
66
|
+
check_field(['--username', usr], :username, usr)
|
67
|
+
end
|
68
|
+
it 'is expected an error when argument not given' do
|
69
|
+
check_field_err(['-u'], OptionParser::MissingArgument)
|
70
|
+
check_field_err(['--username'], OptionParser::MissingArgument)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '-c' do
|
75
|
+
it 'is accepted' do
|
76
|
+
file = './test'
|
77
|
+
check_field(['-c', file], :config, file)
|
78
|
+
check_field(['--config', file], :config, file)
|
79
|
+
end
|
80
|
+
it 'is expected an error when argument not given' do
|
81
|
+
check_field_err(['-c'])
|
82
|
+
check_field_err(['--config'])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module JiraReport
|
4
|
+
describe JiraReport do
|
5
|
+
let(:jrep) { Reporter.new('url', 'admin', '*****') }
|
6
|
+
|
7
|
+
describe '#query_issues' do
|
8
|
+
it 'should throw exception' do
|
9
|
+
begin
|
10
|
+
jrep.query_issues('my_jql')
|
11
|
+
fail Exception, 'exception expected'
|
12
|
+
rescue => e
|
13
|
+
expect(e.message).not_to be nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#jira_api_url' do
|
19
|
+
def jira_api_url(url=nil, user=nil, pass=nil)
|
20
|
+
jrep.send(:jira_api_url, url, user, pass)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'is an http url' do
|
24
|
+
url = jira_api_url
|
25
|
+
expect(url).not_to be nil
|
26
|
+
expect(url).to start_with 'http'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'uses rest api' do
|
30
|
+
expect(jira_api_url).to include('rest/api/2/search')
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when we use url, username and password' do
|
34
|
+
let(:usr) { 'admin_username' }
|
35
|
+
let(:url) { 'jira.mycompany.url' }
|
36
|
+
let(:pas) { '*****' }
|
37
|
+
it 'contains url, username and password' do
|
38
|
+
url = jira_api_url(url, usr, pas)
|
39
|
+
expect(url).to include(url)
|
40
|
+
expect(url).to include(usr)
|
41
|
+
expect(url).to include(pas)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '#queries' do
|
48
|
+
let(:usr) { 'JohnDoe' }
|
49
|
+
let(:from) { '-3w' }
|
50
|
+
let(:till) { '-1w' }
|
51
|
+
|
52
|
+
mocked_rep = Reporter.new('url', 'usr', 'pas')
|
53
|
+
|
54
|
+
before(:example) do
|
55
|
+
allow(mocked_rep).to receive(:query_issues) do |arg|
|
56
|
+
arg
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#created' do
|
61
|
+
it 'has from, till and reporter' do
|
62
|
+
expect(mocked_rep.created(usr, from, till)).to eql(
|
63
|
+
"jql=created>=#{from} AND created<=#{till} AND reporter=#{usr}"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
it 'has from and reporter' do
|
67
|
+
expect(mocked_rep.created(usr, from)).to eql(
|
68
|
+
"jql=created>=#{from} AND reporter=#{usr}"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
it 'has till and reporter' do
|
72
|
+
expect(mocked_rep.created(usr, nil, till)).to eql(
|
73
|
+
"jql=created<=#{till} AND reporter=#{usr}"
|
74
|
+
)
|
75
|
+
end
|
76
|
+
it 'has only reporter' do
|
77
|
+
expect(mocked_rep.created(usr)).to eql(
|
78
|
+
"jql=reporter=#{usr}"
|
79
|
+
)
|
80
|
+
end
|
81
|
+
it 'has not parameters' do
|
82
|
+
expect(mocked_rep.created(nil)).to eql(
|
83
|
+
"jql=reporter="
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe '#resolved' do
|
89
|
+
it 'has from, till and reporter' do
|
90
|
+
expect(mocked_rep.resolved(usr, from, till)).to eql(
|
91
|
+
"jql=resolved>=#{from} AND resolved<=#{till} AND 'First Resolution User'=#{usr}"
|
92
|
+
)
|
93
|
+
end
|
94
|
+
it 'has from and reporter' do
|
95
|
+
expect(mocked_rep.resolved(usr, from)).to eql(
|
96
|
+
"jql=resolved>=#{from} AND 'First Resolution User'=#{usr}"
|
97
|
+
)
|
98
|
+
end
|
99
|
+
it 'has till and reporter' do
|
100
|
+
expect(mocked_rep.resolved(usr, nil, till)).to eql(
|
101
|
+
"jql=resolved<=#{till} AND 'First Resolution User'=#{usr}"
|
102
|
+
)
|
103
|
+
end
|
104
|
+
it 'has only reporter' do
|
105
|
+
expect(mocked_rep.resolved(usr)).to eql(
|
106
|
+
"jql='First Resolution User'=#{usr}"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
it 'has not parameters' do
|
110
|
+
expect(mocked_rep.resolved(nil)).to eql(
|
111
|
+
"jql='First Resolution User'="
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '#reopened' do
|
117
|
+
it 'has from, till and reporter' do
|
118
|
+
expect(mocked_rep.reopened(usr, from, till)).to eql(
|
119
|
+
"jql='First Reopened Date'>=#{from} "\
|
120
|
+
"AND 'First Reopened Date'<=#{till} "\
|
121
|
+
"AND 'First Reopened User'=#{usr}"
|
122
|
+
)
|
123
|
+
end
|
124
|
+
it 'has from and reporter' do
|
125
|
+
expect(mocked_rep.reopened(usr, from)).to eql(
|
126
|
+
"jql='First Reopened Date'>=#{from} "\
|
127
|
+
"AND 'First Reopened User'=#{usr}"
|
128
|
+
)
|
129
|
+
end
|
130
|
+
it 'has till and reporter' do
|
131
|
+
expect(mocked_rep.reopened(usr, nil, till)).to eql(
|
132
|
+
"jql='First Reopened Date'<=#{till} "\
|
133
|
+
"AND 'First Reopened User'=#{usr}"
|
134
|
+
)
|
135
|
+
end
|
136
|
+
it 'has only reporter' do
|
137
|
+
expect(mocked_rep.reopened(usr)).to eql(
|
138
|
+
"jql='First Reopened User'=#{usr}"
|
139
|
+
)
|
140
|
+
end
|
141
|
+
it 'has not parameters' do
|
142
|
+
expect(mocked_rep.reopened(nil)).to eql(
|
143
|
+
"jql='First Reopened User'="
|
144
|
+
)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#closed' do
|
149
|
+
it 'has from, till and reporter' do
|
150
|
+
expect(mocked_rep.closed(usr, from, till)).to eql(
|
151
|
+
"jql='First Closed Date'>=#{from} "\
|
152
|
+
"AND 'First Closed Date'<=#{till} "\
|
153
|
+
"AND 'First Closed User'=#{usr}"
|
154
|
+
)
|
155
|
+
end
|
156
|
+
it 'has from and reporter' do
|
157
|
+
expect(mocked_rep.closed(usr, from)).to eql(
|
158
|
+
"jql='First Closed Date'>=#{from} "\
|
159
|
+
"AND 'First Closed User'=#{usr}"
|
160
|
+
)
|
161
|
+
end
|
162
|
+
it 'has till and reporter' do
|
163
|
+
expect(mocked_rep.closed(usr, nil, till)).to eql(
|
164
|
+
"jql='First Closed Date'<=#{till} "\
|
165
|
+
"AND 'First Closed User'=#{usr}"
|
166
|
+
)
|
167
|
+
end
|
168
|
+
it 'has only reporter' do
|
169
|
+
expect(mocked_rep.closed(usr)).to eql(
|
170
|
+
"jql='First Closed User'=#{usr}"
|
171
|
+
)
|
172
|
+
end
|
173
|
+
it 'has not parameters' do
|
174
|
+
expect(mocked_rep.closed(nil)).to eql(
|
175
|
+
"jql='First Closed User'="
|
176
|
+
)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jira_report
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vitalii Elenhaupt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-02-
|
11
|
+
date: 2015-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: parseconfig
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '1.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '1.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rest-client
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,9 +99,13 @@ files:
|
|
99
99
|
- jira_report.gemspec
|
100
100
|
- lib/jira_report.rb
|
101
101
|
- lib/jira_report/cli.rb
|
102
|
-
- lib/jira_report/
|
102
|
+
- lib/jira_report/config_loader.rb
|
103
|
+
- lib/jira_report/options.rb
|
104
|
+
- lib/jira_report/reporter.rb
|
103
105
|
- lib/jira_report/version.rb
|
104
|
-
- spec/
|
106
|
+
- spec/jira_report/config_loader_spec.rb
|
107
|
+
- spec/jira_report/options_spec.rb
|
108
|
+
- spec/jira_report/reporter_spec.rb
|
105
109
|
- spec/spec_helper.rb
|
106
110
|
homepage: https://github.com/veelenga/jira_report.git
|
107
111
|
licenses:
|
@@ -115,7 +119,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
119
|
requirements:
|
116
120
|
- - ">="
|
117
121
|
- !ruby/object:Gem::Version
|
118
|
-
version:
|
122
|
+
version: 1.9.3
|
119
123
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
124
|
requirements:
|
121
125
|
- - ">="
|
@@ -128,6 +132,8 @@ signing_key:
|
|
128
132
|
specification_version: 4
|
129
133
|
summary: Jira activity report generator.
|
130
134
|
test_files:
|
131
|
-
- spec/
|
135
|
+
- spec/jira_report/config_loader_spec.rb
|
136
|
+
- spec/jira_report/options_spec.rb
|
137
|
+
- spec/jira_report/reporter_spec.rb
|
132
138
|
- spec/spec_helper.rb
|
133
139
|
has_rdoc:
|
data/lib/jira_report/settings.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
require 'inifile'
|
2
|
-
require 'io/console'
|
3
|
-
|
4
|
-
# Initializes JiraReport settings.
|
5
|
-
module JiraReport
|
6
|
-
class Settings
|
7
|
-
attr_reader :username, :password, :url,\
|
8
|
-
:period_from, :period_till
|
9
|
-
|
10
|
-
def initialize(path)
|
11
|
-
read(path)
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def read(path)
|
17
|
-
loaded = IniFile.load(File.expand_path(path))
|
18
|
-
|
19
|
-
global = loaded[:global] if loaded
|
20
|
-
global ||= {}
|
21
|
-
|
22
|
-
@url = global['url']
|
23
|
-
@username = global['username']
|
24
|
-
@password = global['password']
|
25
|
-
@period_from = global['period_from']
|
26
|
-
@period_till = global['period_till']
|
27
|
-
|
28
|
-
@url = ask('Jira url in [jira.company.com] format: ') unless @url
|
29
|
-
@username = ask('Jira username: ') unless @username
|
30
|
-
@password = ask('Jira password: '){
|
31
|
-
STDIN.noecho(&:gets).chomp!
|
32
|
-
} unless @password
|
33
|
-
@period_from = '-1w' unless @period_from
|
34
|
-
@period_till = 'now()' unless @period_till
|
35
|
-
|
36
|
-
# TODO: attributes verification
|
37
|
-
end
|
38
|
-
|
39
|
-
def ask(message, &block)
|
40
|
-
print message
|
41
|
-
if block
|
42
|
-
yield block
|
43
|
-
else
|
44
|
-
gets.chomp!
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
data/spec/jira_report_spec.rb
DELETED
@@ -1,64 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module JiraReport
|
4
|
-
|
5
|
-
class InitSet
|
6
|
-
attr_accessor :url,
|
7
|
-
:username, :password,
|
8
|
-
:period_from, :period_till
|
9
|
-
end
|
10
|
-
|
11
|
-
describe JiraReport do
|
12
|
-
let(:init) { InitSet.new }
|
13
|
-
let(:jrep) { JiraReport.new(init, 'username') }
|
14
|
-
|
15
|
-
describe '#jira_search_url' do
|
16
|
-
def jira_search_url(url=nil, user=nil, pass=nil)
|
17
|
-
jrep.send(:jira_search_url, url, user, pass)
|
18
|
-
end
|
19
|
-
|
20
|
-
it 'is an http url' do
|
21
|
-
url = jira_search_url
|
22
|
-
expect(url).not_to be nil
|
23
|
-
expect(url).to start_with 'http'
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'uses rest api' do
|
27
|
-
expect(jira_search_url).to include('rest/api/2/search')
|
28
|
-
end
|
29
|
-
|
30
|
-
context 'when we use url and username' do
|
31
|
-
let(:usr) { 'admin_username' }
|
32
|
-
let(:url) { 'jira.mycompany.url' }
|
33
|
-
it 'contains url and username' do
|
34
|
-
url = jira_search_url(url, usr, nil)
|
35
|
-
expect(url).to include(url)
|
36
|
-
expect(url).to include(usr)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
describe '#jql_*' do
|
41
|
-
describe '#jql_created' do
|
42
|
-
it 'returns query' do
|
43
|
-
expect(jrep.send(:jql_created)).not_to be nil
|
44
|
-
end
|
45
|
-
end
|
46
|
-
describe '#jql_resolved' do
|
47
|
-
it 'returns query' do
|
48
|
-
expect(jrep.send(:jql_resolved)).not_to be nil
|
49
|
-
end
|
50
|
-
end
|
51
|
-
describe '#jql_closed' do
|
52
|
-
it 'returns query' do
|
53
|
-
expect(jrep.send(:jql_closed)).not_to be nil
|
54
|
-
end
|
55
|
-
end
|
56
|
-
describe '#jql_reopened' do
|
57
|
-
it 'returns query' do
|
58
|
-
expect(jrep.send(:jql_reopened)).not_to be nil
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|