tempest-time 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE +21 -0
- data/README.md +8 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/tempest +18 -0
- data/lib/tempest_time.rb +4 -0
- data/lib/tempest_time/api/authorization.rb +17 -0
- data/lib/tempest_time/api/jira_api/authorization.rb +27 -0
- data/lib/tempest_time/api/jira_api/models/issue.rb +28 -0
- data/lib/tempest_time/api/jira_api/request.rb +40 -0
- data/lib/tempest_time/api/jira_api/requests/get_issue.rb +29 -0
- data/lib/tempest_time/api/jira_api/requests/get_user_issues.rb +36 -0
- data/lib/tempest_time/api/jira_api/response.rb +5 -0
- data/lib/tempest_time/api/jira_api/responses/get_issue.rb +16 -0
- data/lib/tempest_time/api/jira_api/responses/get_user_issues.rb +14 -0
- data/lib/tempest_time/api/request.rb +87 -0
- data/lib/tempest_time/api/response.rb +34 -0
- data/lib/tempest_time/api/tempo_api/authorization.rb +23 -0
- data/lib/tempest_time/api/tempo_api/models/worklog.rb +36 -0
- data/lib/tempest_time/api/tempo_api/request.rb +35 -0
- data/lib/tempest_time/api/tempo_api/requests/create_worklog.rb +51 -0
- data/lib/tempest_time/api/tempo_api/requests/delete_worklog.rb +29 -0
- data/lib/tempest_time/api/tempo_api/requests/get_worklog.rb +29 -0
- data/lib/tempest_time/api/tempo_api/requests/list_worklogs.rb +41 -0
- data/lib/tempest_time/api/tempo_api/requests/report.rb +0 -0
- data/lib/tempest_time/api/tempo_api/requests/submit_timesheet.rb +49 -0
- data/lib/tempest_time/api/tempo_api/response.rb +5 -0
- data/lib/tempest_time/api/tempo_api/responses/create_worklog.rb +28 -0
- data/lib/tempest_time/api/tempo_api/responses/delete_worklog.rb +8 -0
- data/lib/tempest_time/api/tempo_api/responses/get_worklog.rb +22 -0
- data/lib/tempest_time/api/tempo_api/responses/list_worklogs.rb +40 -0
- data/lib/tempest_time/api/tempo_api/responses/submit_timesheet.rb +13 -0
- data/lib/tempest_time/cli.rb +92 -0
- data/lib/tempest_time/command.rb +60 -0
- data/lib/tempest_time/commands/config.rb +30 -0
- data/lib/tempest_time/commands/config/edit.rb +38 -0
- data/lib/tempest_time/commands/config/setup.rb +55 -0
- data/lib/tempest_time/commands/delete.rb +31 -0
- data/lib/tempest_time/commands/issues.rb +37 -0
- data/lib/tempest_time/commands/list.rb +32 -0
- data/lib/tempest_time/commands/report.rb +83 -0
- data/lib/tempest_time/commands/submit.rb +26 -0
- data/lib/tempest_time/commands/teams.rb +30 -0
- data/lib/tempest_time/commands/teams/add.rb +29 -0
- data/lib/tempest_time/commands/teams/delete.rb +32 -0
- data/lib/tempest_time/commands/teams/edit.rb +49 -0
- data/lib/tempest_time/commands/track.rb +70 -0
- data/lib/tempest_time/helpers/formatting_helper.rb +16 -0
- data/lib/tempest_time/helpers/time_helper.rb +63 -0
- data/lib/tempest_time/models/report.rb +58 -0
- data/lib/tempest_time/services/generate_report.rb +54 -0
- data/lib/tempest_time/setting.rb +47 -0
- data/lib/tempest_time/settings/authorization.rb +19 -0
- data/lib/tempest_time/settings/teams.rb +19 -0
- data/lib/tempest_time/templates/config/.gitkeep +1 -0
- data/lib/tempest_time/templates/config/auth/.gitkeep +1 -0
- data/lib/tempest_time/templates/config/setup/.gitkeep +1 -0
- data/lib/tempest_time/templates/config/teams/.gitkeep +1 -0
- data/lib/tempest_time/templates/delete/.gitkeep +1 -0
- data/lib/tempest_time/templates/issue/.gitkeep +1 -0
- data/lib/tempest_time/templates/issues/.gitkeep +1 -0
- data/lib/tempest_time/templates/list/.gitkeep +1 -0
- data/lib/tempest_time/templates/report/.gitkeep +1 -0
- data/lib/tempest_time/templates/setup/.gitkeep +1 -0
- data/lib/tempest_time/templates/submit/.gitkeep +1 -0
- data/lib/tempest_time/templates/teams/.gitkeep +1 -0
- data/lib/tempest_time/templates/track/.gitkeep +1 -0
- data/lib/tempest_time/templates/view/.gitkeep +1 -0
- data/lib/tempest_time/version.rb +3 -0
- data/tempest-time.gemspec +62 -0
- metadata +517 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative '../response'
|
2
|
+
require_relative '../../../helpers/time_helper'
|
3
|
+
|
4
|
+
module TempoAPI
|
5
|
+
module Responses
|
6
|
+
class CreateWorklog < TempoAPI::Response
|
7
|
+
include TempestTime::Helpers::TimeHelper
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def worklog_id
|
12
|
+
@worklog_id ||= raw_response['tempoWorklogId']
|
13
|
+
end
|
14
|
+
|
15
|
+
def seconds
|
16
|
+
@seconds ||= raw_response['timeSpentSeconds']
|
17
|
+
end
|
18
|
+
|
19
|
+
def issue_key
|
20
|
+
@issue_key ||= raw_response['issue']['key']
|
21
|
+
end
|
22
|
+
|
23
|
+
def success_message
|
24
|
+
"Worklog #{worklog_id} created!"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative '../response'
|
2
|
+
|
3
|
+
module TempoAPI
|
4
|
+
module Responses
|
5
|
+
class GetWorklog < TempoAPI::Response
|
6
|
+
private
|
7
|
+
|
8
|
+
def worklog
|
9
|
+
@worklog ||= TempoAPI::Models::Worklog.new(
|
10
|
+
id: raw_response['tempoWorklogId'],
|
11
|
+
issue: raw_response.dig('issue', 'key'),
|
12
|
+
seconds: raw_response['timeSpentSeconds'],
|
13
|
+
description: raw_response['description']
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def success_message
|
18
|
+
worklog.to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative '../response'
|
2
|
+
require_relative '../models/worklog'
|
3
|
+
require_relative '../../../helpers/time_helper'
|
4
|
+
|
5
|
+
module TempoAPI
|
6
|
+
module Responses
|
7
|
+
class ListWorklogs < TempoAPI::Response
|
8
|
+
include TempestTime::Helpers::TimeHelper
|
9
|
+
|
10
|
+
attr_reader :worklogs
|
11
|
+
|
12
|
+
def worklogs
|
13
|
+
@worklogs ||= raw_response['results'].map do |worklog|
|
14
|
+
TempoAPI::Models::Worklog.new(
|
15
|
+
id: worklog['tempoWorklogId'],
|
16
|
+
issue: worklog.dig('issue', 'key'),
|
17
|
+
seconds: worklog['timeSpentSeconds'],
|
18
|
+
description: worklog['description']
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def success_message
|
26
|
+
output = ""
|
27
|
+
output << worklogs_output
|
28
|
+
output << "\nTOTAL TIME LOGGED: #{total_hours_spent} hours."
|
29
|
+
end
|
30
|
+
|
31
|
+
def worklogs_output
|
32
|
+
worklogs.map(&:to_s).join("\n")
|
33
|
+
end
|
34
|
+
|
35
|
+
def total_hours_spent
|
36
|
+
worklogs.map(&:hours).reduce(:+)&.round(2) || 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'git'
|
3
|
+
|
4
|
+
module TempestTime
|
5
|
+
class CLI < Thor
|
6
|
+
# Error raised by this runner
|
7
|
+
Error = Class.new(StandardError)
|
8
|
+
|
9
|
+
desc 'version', 'Show version number.'
|
10
|
+
def version
|
11
|
+
require_relative 'version'
|
12
|
+
puts "v#{TempestTime::VERSION}"
|
13
|
+
end
|
14
|
+
map %w(--version -v) => :version
|
15
|
+
|
16
|
+
require_relative 'commands/config'
|
17
|
+
register TempestTime::Commands::Config,
|
18
|
+
'config', 'config [SUBCOMMAND]',
|
19
|
+
'Setup or modify user settings.'
|
20
|
+
|
21
|
+
require_relative 'commands/teams'
|
22
|
+
register TempestTime::Commands::Teams,
|
23
|
+
'teams', 'teams [SUBCOMMAND]',
|
24
|
+
'Add or modify teams.'
|
25
|
+
|
26
|
+
desc 'list [DATE]', 'List worklogs for given date. (Defaults to today.)'
|
27
|
+
long_desc <<-LONGDESC
|
28
|
+
`tempest list` will list a day's worklogs.\n
|
29
|
+
e.g. `tempest list today`\n
|
30
|
+
e.g. `tempest list yesterday`\n
|
31
|
+
e.g. `tempest list 2019-01-31`\n
|
32
|
+
e.g. `tempest list 2019-01-31 --user=jsmith`
|
33
|
+
LONGDESC
|
34
|
+
def list(date = nil)
|
35
|
+
require_relative 'commands/list'
|
36
|
+
TempestTime::Commands::List.new(date, options).execute
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
desc 'submit', 'Submit your timesheet to a supervisor.'
|
41
|
+
def submit(*)
|
42
|
+
require_relative 'commands/submit'
|
43
|
+
TempestTime::Commands::Submit.new(options).execute
|
44
|
+
end
|
45
|
+
|
46
|
+
desc 'report', 'Generate a user or team report.'
|
47
|
+
option :week, aliases: '-w', type: :numeric
|
48
|
+
option :team, aliases: '-t', type: :string
|
49
|
+
def report(*users)
|
50
|
+
require_relative 'commands/report'
|
51
|
+
TempestTime::Commands::Report.new(users, options).execute
|
52
|
+
end
|
53
|
+
|
54
|
+
desc 'delete [WORKLOG(S)]', 'Delete worklogs.'
|
55
|
+
long_desc <<-LONGDESC
|
56
|
+
`tempest_time delete` will delete the specified worklogs.\n
|
57
|
+
e.g. `tempest delete 12345`\n
|
58
|
+
e.g. `tempest delete 12345 12346 12347`\n
|
59
|
+
LONGDESC
|
60
|
+
option :autoconfirm, type: :boolean
|
61
|
+
def delete(*worklogs)
|
62
|
+
require_relative 'commands/delete'
|
63
|
+
TempestTime::Commands::Delete.new(worklogs, options).execute
|
64
|
+
end
|
65
|
+
|
66
|
+
desc 'issues', "View a list of a user's assigned tickets. (Defaults to you.)"
|
67
|
+
def issues(user = nil)
|
68
|
+
require_relative 'commands/issues'
|
69
|
+
TempestTime::Commands::Issues.new(user, options).execute
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "track [TIME] [TICKET(S)]", 'Track time to Tempo.'
|
73
|
+
long_desc <<-LONGDESC
|
74
|
+
`tempest track` or `tempest log` will track the specified number of hours or minutes to the ticket(s) specified.\n
|
75
|
+
If not specified, it will check the name of the current git branch and automatically track the logged time to that ticket, if found.\n
|
76
|
+
You can also split a bank of time evenly across multiple tickets with the --split flag.\n
|
77
|
+
e.g. tempest track 1.5h BCIT-1 --message='Tracking 1.5 hours.'\n
|
78
|
+
e.g. tempest log 90m BCIT-1 BCIT-2 --message='Tracking 90 minutes.'\n
|
79
|
+
e.g. tempest track 3h BCIT-1 BCIT-2 BCIT-3 --message='Tracking 1 hour.'\n
|
80
|
+
LONGDESC
|
81
|
+
option :message, aliases: '-m', type: :string
|
82
|
+
option :date, aliases: '-d', type: :string
|
83
|
+
option :remaining, aliases: '-r', type: :string
|
84
|
+
option :billable, aliases: '-b', type: :boolean, default: true
|
85
|
+
option :split, aliases: '-s', type: :boolean, default: false
|
86
|
+
option :autoconfirm, type: :boolean, default: false
|
87
|
+
def track(time, *tickets)
|
88
|
+
require_relative 'commands/track'
|
89
|
+
TempestTime::Commands::Track.new(time, tickets, options).execute
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module TempestTime
|
6
|
+
class Command
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :command, :run
|
10
|
+
|
11
|
+
def execute(*)
|
12
|
+
raise(
|
13
|
+
NotImplementedError,
|
14
|
+
"#{self.class}##{__method__} must be implemented"
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def command
|
19
|
+
require 'tty-command'
|
20
|
+
TTY::Command.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def pastel(**options)
|
24
|
+
require 'pastel'
|
25
|
+
Pastel.new(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def prompt(**options)
|
29
|
+
require 'tty-prompt'
|
30
|
+
TTY::Prompt.new(options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def spinner
|
34
|
+
require 'tty-spinner'
|
35
|
+
TTY::Spinner
|
36
|
+
end
|
37
|
+
|
38
|
+
def table
|
39
|
+
require 'tty-table'
|
40
|
+
TTY::Table
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_spinner(message, format = :pong)
|
44
|
+
s = spinner.new(":spinner #{message}", format: format)
|
45
|
+
s.auto_spin
|
46
|
+
yield(s)
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_success_fail_spinner(message, format = :spin_3)
|
50
|
+
s = spinner.new(":spinner #{message}", format: format)
|
51
|
+
s.auto_spin
|
52
|
+
response = yield
|
53
|
+
if response.success?
|
54
|
+
s.success(pastel.green(response.message))
|
55
|
+
else
|
56
|
+
s.error(pastel.red(response.message))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module TempestTime
|
6
|
+
module Commands
|
7
|
+
class Config < Thor
|
8
|
+
|
9
|
+
namespace :config
|
10
|
+
|
11
|
+
desc 'setup', 'Set up Tempest with your credentials.'
|
12
|
+
def setup(*)
|
13
|
+
require_relative 'config/setup'
|
14
|
+
TempestTime::Commands::Config::Setup.new(options).execute
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'edit', 'Modify your user credentials.'
|
18
|
+
method_option :help, aliases: '-h', type: :boolean,
|
19
|
+
desc: 'Display usage information'
|
20
|
+
def edit(*)
|
21
|
+
if options[:help]
|
22
|
+
invoke :help, ['auth']
|
23
|
+
else
|
24
|
+
require_relative 'config/edit'
|
25
|
+
TempestTime::Commands::Config::Edit.new(options).execute
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../command'
|
4
|
+
require_relative '../../settings/authorization'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Config
|
9
|
+
class Edit < TempestTime::Command
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
settings = TempestTime::Settings::Authorization
|
16
|
+
setting = prompt.select(
|
17
|
+
"Which #{pastel.green('setting')} would you like to edit?",
|
18
|
+
settings.keys
|
19
|
+
)
|
20
|
+
|
21
|
+
new_value = prompt.ask(
|
22
|
+
"Enter the #{pastel.green('new value')}.",
|
23
|
+
default: settings.read(setting)
|
24
|
+
)
|
25
|
+
|
26
|
+
if new_value != settings.read(setting)
|
27
|
+
settings.update(setting, new_value)
|
28
|
+
prompt.say(pastel.green('Setting updated!'))
|
29
|
+
else
|
30
|
+
prompt.say('Nothing changed.')
|
31
|
+
end
|
32
|
+
|
33
|
+
execute if prompt.yes?('Keep editing?')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../command'
|
4
|
+
require_relative '../../settings/authorization'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Config
|
9
|
+
class Setup < TempestTime::Command
|
10
|
+
def initialize(options)
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
email = prompt.ask(
|
16
|
+
'Please enter your Atlassian ID. '\
|
17
|
+
'This is typically your login email.'
|
18
|
+
)
|
19
|
+
username = prompt.ask(
|
20
|
+
'Please enter your Atlassian username.'
|
21
|
+
)
|
22
|
+
subdomain = prompt.ask(
|
23
|
+
'Please enter your Atlassian subdomain. '\
|
24
|
+
'i.e. [THIS VALUE].atlassian.net'
|
25
|
+
)
|
26
|
+
jira_token = prompt.ask(
|
27
|
+
'Please enter your Atlassian API token. '\
|
28
|
+
'Your token can be generated at https://id.atlassian.com/manage/api-tokens'
|
29
|
+
)
|
30
|
+
tempo_token = prompt.ask(
|
31
|
+
'Please enter your Tempo API token. '\
|
32
|
+
"Your token can be generated through your worksheet's settings page."
|
33
|
+
)
|
34
|
+
|
35
|
+
if [email, username, subdomain, jira_token, tempo_token].any?(&:nil?)
|
36
|
+
abort(
|
37
|
+
pastel.red('Setup failed!') + ' ' +
|
38
|
+
'One or more credentials were missing. Please try again.'
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
authorization = TempestTime::Settings::Authorization
|
43
|
+
|
44
|
+
authorization.update('email', email)
|
45
|
+
authorization.update('username', username)
|
46
|
+
authorization.update('subdomain', subdomain)
|
47
|
+
authorization.update('jira_token', jira_token)
|
48
|
+
authorization.update('tempo_token', tempo_token)
|
49
|
+
|
50
|
+
puts pastel.green('Setup complete!')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
require_relative '../api/tempo_api/requests/delete_worklog'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Delete < TempestTime::Command
|
9
|
+
def initialize(worklogs, options)
|
10
|
+
@worklogs = worklogs
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
pluralized = @worklogs.length > 1 ? 'worklogs' : 'worklog'
|
16
|
+
confirm_message =
|
17
|
+
"Delete #{pluralized} #{pastel.green(@worklogs.join(', '))}?"
|
18
|
+
abort unless prompt.yes?(confirm_message)
|
19
|
+
@worklogs.each { |worklog| delete_worklog(worklog) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def delete_worklog(worklog)
|
25
|
+
with_success_fail_spinner("Deleting worklog #{worklog}...", :spin_3) do
|
26
|
+
TempoAPI::Requests::DeleteWorklog.new(worklog).send_request
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
require_relative '../api/jira_api/requests/get_user_issues'
|
5
|
+
|
6
|
+
module TempestTime
|
7
|
+
module Commands
|
8
|
+
class Issues < TempestTime::Command
|
9
|
+
def initialize(user, options)
|
10
|
+
@user = user
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(input: $stdin, output: $stdout)
|
15
|
+
request = JiraAPI::Requests::GetUserIssues.new(@user)
|
16
|
+
with_spinner("Getting issues for #{request.requested_user}") do |spinner|
|
17
|
+
response = request.send_request
|
18
|
+
spinner.stop
|
19
|
+
puts format_output(response.issues)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def format_output(issues)
|
26
|
+
table.new(
|
27
|
+
%w[Status Issue Summary],
|
28
|
+
issues.map { |issue| row(issue) }
|
29
|
+
).render(:ascii, padding: [0, 1])
|
30
|
+
end
|
31
|
+
|
32
|
+
def row(issue)
|
33
|
+
[issue.status, issue.key, issue.summary]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|