tempest-time 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +133 -0
  7. data/LICENSE +21 -0
  8. data/README.md +8 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/bin/tempest +18 -0
  13. data/lib/tempest_time.rb +4 -0
  14. data/lib/tempest_time/api/authorization.rb +17 -0
  15. data/lib/tempest_time/api/jira_api/authorization.rb +27 -0
  16. data/lib/tempest_time/api/jira_api/models/issue.rb +28 -0
  17. data/lib/tempest_time/api/jira_api/request.rb +40 -0
  18. data/lib/tempest_time/api/jira_api/requests/get_issue.rb +29 -0
  19. data/lib/tempest_time/api/jira_api/requests/get_user_issues.rb +36 -0
  20. data/lib/tempest_time/api/jira_api/response.rb +5 -0
  21. data/lib/tempest_time/api/jira_api/responses/get_issue.rb +16 -0
  22. data/lib/tempest_time/api/jira_api/responses/get_user_issues.rb +14 -0
  23. data/lib/tempest_time/api/request.rb +87 -0
  24. data/lib/tempest_time/api/response.rb +34 -0
  25. data/lib/tempest_time/api/tempo_api/authorization.rb +23 -0
  26. data/lib/tempest_time/api/tempo_api/models/worklog.rb +36 -0
  27. data/lib/tempest_time/api/tempo_api/request.rb +35 -0
  28. data/lib/tempest_time/api/tempo_api/requests/create_worklog.rb +51 -0
  29. data/lib/tempest_time/api/tempo_api/requests/delete_worklog.rb +29 -0
  30. data/lib/tempest_time/api/tempo_api/requests/get_worklog.rb +29 -0
  31. data/lib/tempest_time/api/tempo_api/requests/list_worklogs.rb +41 -0
  32. data/lib/tempest_time/api/tempo_api/requests/report.rb +0 -0
  33. data/lib/tempest_time/api/tempo_api/requests/submit_timesheet.rb +49 -0
  34. data/lib/tempest_time/api/tempo_api/response.rb +5 -0
  35. data/lib/tempest_time/api/tempo_api/responses/create_worklog.rb +28 -0
  36. data/lib/tempest_time/api/tempo_api/responses/delete_worklog.rb +8 -0
  37. data/lib/tempest_time/api/tempo_api/responses/get_worklog.rb +22 -0
  38. data/lib/tempest_time/api/tempo_api/responses/list_worklogs.rb +40 -0
  39. data/lib/tempest_time/api/tempo_api/responses/submit_timesheet.rb +13 -0
  40. data/lib/tempest_time/cli.rb +92 -0
  41. data/lib/tempest_time/command.rb +60 -0
  42. data/lib/tempest_time/commands/config.rb +30 -0
  43. data/lib/tempest_time/commands/config/edit.rb +38 -0
  44. data/lib/tempest_time/commands/config/setup.rb +55 -0
  45. data/lib/tempest_time/commands/delete.rb +31 -0
  46. data/lib/tempest_time/commands/issues.rb +37 -0
  47. data/lib/tempest_time/commands/list.rb +32 -0
  48. data/lib/tempest_time/commands/report.rb +83 -0
  49. data/lib/tempest_time/commands/submit.rb +26 -0
  50. data/lib/tempest_time/commands/teams.rb +30 -0
  51. data/lib/tempest_time/commands/teams/add.rb +29 -0
  52. data/lib/tempest_time/commands/teams/delete.rb +32 -0
  53. data/lib/tempest_time/commands/teams/edit.rb +49 -0
  54. data/lib/tempest_time/commands/track.rb +70 -0
  55. data/lib/tempest_time/helpers/formatting_helper.rb +16 -0
  56. data/lib/tempest_time/helpers/time_helper.rb +63 -0
  57. data/lib/tempest_time/models/report.rb +58 -0
  58. data/lib/tempest_time/services/generate_report.rb +54 -0
  59. data/lib/tempest_time/setting.rb +47 -0
  60. data/lib/tempest_time/settings/authorization.rb +19 -0
  61. data/lib/tempest_time/settings/teams.rb +19 -0
  62. data/lib/tempest_time/templates/config/.gitkeep +1 -0
  63. data/lib/tempest_time/templates/config/auth/.gitkeep +1 -0
  64. data/lib/tempest_time/templates/config/setup/.gitkeep +1 -0
  65. data/lib/tempest_time/templates/config/teams/.gitkeep +1 -0
  66. data/lib/tempest_time/templates/delete/.gitkeep +1 -0
  67. data/lib/tempest_time/templates/issue/.gitkeep +1 -0
  68. data/lib/tempest_time/templates/issues/.gitkeep +1 -0
  69. data/lib/tempest_time/templates/list/.gitkeep +1 -0
  70. data/lib/tempest_time/templates/report/.gitkeep +1 -0
  71. data/lib/tempest_time/templates/setup/.gitkeep +1 -0
  72. data/lib/tempest_time/templates/submit/.gitkeep +1 -0
  73. data/lib/tempest_time/templates/teams/.gitkeep +1 -0
  74. data/lib/tempest_time/templates/track/.gitkeep +1 -0
  75. data/lib/tempest_time/templates/view/.gitkeep +1 -0
  76. data/lib/tempest_time/version.rb +3 -0
  77. data/tempest-time.gemspec +62 -0
  78. metadata +517 -0
@@ -0,0 +1,5 @@
1
+ require_relative '../response'
2
+
3
+ module TempoAPI
4
+ class Response < TempestTime::API::Response; end
5
+ end
@@ -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,8 @@
1
+ require_relative '../response'
2
+
3
+ module TempoAPI
4
+ module Responses
5
+ class DeleteWorklog < TempoAPI::Response
6
+ end
7
+ end
8
+ 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,13 @@
1
+ require_relative '../response'
2
+
3
+ module TempoAPI
4
+ module Responses
5
+ class SubmitTimesheet < TempoAPI::Response
6
+ private
7
+
8
+ def success_message
9
+ "Timesheet submitted successfully to #{request.reviewer}!"
10
+ end
11
+ end
12
+ end
13
+ 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