durt 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml/store'
4
+
5
+ module Durt
6
+ module Configurable
7
+ STORE_FILE_NAME = '.durt.yml'
8
+ STORE_FILE_PATH = File.expand_path("~/#{STORE_FILE_NAME}")
9
+
10
+ def config
11
+ config_store.transaction do
12
+ config_store[config_key]
13
+ end
14
+ end
15
+
16
+ def config!(value)
17
+ config_store.transaction do
18
+ config_store[config_key] = value
19
+ end
20
+ end
21
+
22
+ def config?
23
+ !config.nil?
24
+ end
25
+
26
+ def config_key
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def config_store
31
+ @config_store ||= YAML::Store.new(STORE_FILE_PATH)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plugin'
4
+
5
+ module Durt
6
+ class EbsPlugin < Plugin
7
+ def before_enter(issue)
8
+ return if issue.estimate?
9
+
10
+ edit_estimate(issue)
11
+ end
12
+
13
+ def edit_estimate(issue)
14
+ puts issue.to_s
15
+ estimate_input =
16
+ prompt.ask('How long do you think this task will take you?')
17
+
18
+ input_in_seconds = estimate_input_to_seconds(estimate_input)
19
+
20
+ issue.update(estimate: input_in_seconds)
21
+ issue
22
+ end
23
+
24
+ private
25
+
26
+ def estimate_input_to_seconds(input)
27
+ digit = input.gsub(/[^\d\.]/, '').to_f
28
+ measure_char = input.gsub(/[\d\.]/, '').strip.chr
29
+
30
+ time_in_seconds = if measure_char == 's'
31
+ digit
32
+ elsif measure_char == 'm'
33
+ digit * 60
34
+ elsif measure_char == 'h'
35
+ digit * 3600
36
+ else
37
+ raise WhatKindOfTimeIsThatError
38
+ end
39
+
40
+ time_in_seconds.ceil(2)
41
+ end
42
+
43
+ def prompt
44
+ @prompt ||= TTY::Prompt.new
45
+ end
46
+ end
47
+ end
48
+
49
+ module Durt
50
+ module Command
51
+ class EditEstimate < Durt::Service
52
+ def initialize
53
+ controller = Durt::ProjectController.new
54
+
55
+ steps << ->(_state) { controller.current_issue }
56
+ steps << ->(issue) { controller.edit_estimate(issue) }
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module Durt
63
+ class ProjectController
64
+ def edit_estimate(issue)
65
+ EbsPlugin.new(issue.project).edit_estimate(issue)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bug_tracker'
4
+ require 'octokit'
5
+
6
+ module Durt
7
+ class GithubBugTracker < BugTracker
8
+ attr_accessor :client
9
+
10
+ def after_initialize
11
+ @client = Octokit::Client.new(@config)
12
+ @client.auto_paginate = true
13
+ end
14
+
15
+ def fetch_issues
16
+ fetched_issues = client.issues(fetch_issues_query)
17
+
18
+ fetched_issues.map do |issue|
19
+ {
20
+ key: issue.number,
21
+ summary: issue.title,
22
+ source: source_name,
23
+ project: project
24
+ }
25
+ end
26
+ end
27
+
28
+ # def comment(_key, _content)
29
+ # nil
30
+ # end
31
+
32
+ private
33
+
34
+ def fetch_issues_query
35
+ query = @config[:repo]
36
+ puts query
37
+ query
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plugin'
4
+
5
+ module Durt
6
+ class GithubPlugin < Plugin
7
+ def self.demo_config
8
+ { access_token: '<your 40 char token>', repo: 'rails/rails' }
9
+ end
10
+
11
+ def bug_tracker_class
12
+ Durt::GithubBugTracker
13
+ end
14
+
15
+ def config_required?
16
+ true
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ module Durt
2
+ class GlobalController
3
+ def create_project
4
+ project_name = prompt.ask('What will you name your project?')
5
+
6
+ Durt::Project.create(name: project_name)
7
+ end
8
+
9
+ def create_project_config(project)
10
+ project.tap { |p| p.config!('plugins' => plugins_config) }
11
+ end
12
+
13
+ def select_project(project = nil)
14
+ projects = Durt::Project.all
15
+
16
+ project ||=
17
+ begin
18
+ prompt.select('Select project:', projects.to_choice_h)
19
+ end
20
+
21
+ projects.update_all(active: false)
22
+ project.tap(&:active!)
23
+ end
24
+
25
+ def switch_to_project(project)
26
+ project.tap do |p|
27
+ p.time_tracker_plugins.each(&:switch_project)
28
+ end
29
+ end
30
+
31
+ def console
32
+ binding.pry
33
+ end
34
+
35
+ private
36
+
37
+ def plugins_config
38
+ all_plugins = Durt::Plugin.all
39
+ plugin_choices =
40
+ (all_plugins - [Durt::LocalPlugin])
41
+ .map { |p| [p.plugin_name, p] }
42
+ .to_h
43
+
44
+ prompt
45
+ .multi_select('Select plugins', plugin_choices)
46
+ .push(Durt::LocalPlugin)
47
+ .map { |p| [p.plugin_name, p.demo_config] }
48
+ .to_h
49
+ end
50
+
51
+ def prompt
52
+ @prompt ||= TTY::Prompt.new
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'application_record'
4
+ require 'chronic_duration'
5
+
6
+ module Durt
7
+ class Issue < ApplicationRecord
8
+ before_validation :sanitize_summary
9
+ has_many :sessions, dependent: :destroy
10
+ belongs_to :project
11
+
12
+ scope :active, -> { where(active: true) }
13
+
14
+ def tracking?
15
+ !sessions.tracking.empty?
16
+ end
17
+
18
+ def start_tracking!
19
+ if tracking?
20
+ puts 'Already tracking'
21
+ return sessions.tracking.last
22
+ end
23
+
24
+ sessions.create(open_at: Time.now)
25
+ end
26
+
27
+ def plugin
28
+ project.plugins.find { |p| p.plugin_name == source }
29
+ end
30
+
31
+ def stop_tracking!
32
+ sessions.tracking.update_all(closed_at: Time.now)
33
+ end
34
+
35
+ def total_tracked_time
36
+ sessions.map(&:tracked_time).sum
37
+ end
38
+
39
+ def estimation_ratio
40
+ return Float::INFINITY if total_tracked_time.zero?
41
+
42
+ estimate.to_f / total_tracked_time
43
+ end
44
+
45
+ def overestimated?
46
+ estimation_ratio > 1
47
+ end
48
+
49
+ def underestimated?
50
+ estimation_ratio < 1
51
+ end
52
+
53
+ # Presenters
54
+
55
+ def stats
56
+ <<~MSG
57
+
58
+ -- #{self} --
59
+ Estimated: #{ChronicDuration.output(estimate || 0, format: :long)}.
60
+ Tracked: #{ChronicDuration.output(total_tracked_time, format: :long)}.
61
+ Estimation ratio: #{estimation_ratio} (#{estimation_result_label})
62
+ -----------------------------------
63
+
64
+ MSG
65
+ end
66
+
67
+ def puts_stats
68
+ puts stats
69
+ end
70
+
71
+ def estimation_result_label
72
+ return 'Underestimated' if underestimated?
73
+ return 'Overestimated' if overestimated?
74
+
75
+ 'Who are you?'
76
+ end
77
+
78
+ def label
79
+ "[#{key}]"
80
+ end
81
+
82
+ def to_s
83
+ "#{label} #{summary}"
84
+ end
85
+
86
+ private
87
+
88
+ def sanitize_summary
89
+ return unless summary
90
+
91
+ self.summary = summary.gsub(/[^\d\w\s,]/i, '')
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bug_tracker'
4
+ require 'jira-ruby'
5
+
6
+ module Durt
7
+ class JiraBugTracker < BugTracker
8
+ attr_accessor :client
9
+
10
+ def after_initialize
11
+ @client = JIRA::Client.new(@config)
12
+ end
13
+
14
+ def fetch_issues
15
+ fetched_issues = @client.Issue.jql(fetch_issues_query)
16
+
17
+ fetched_issues.map do |issue|
18
+ {
19
+ key: issue.key,
20
+ summary: issue.summary,
21
+ source: source_name,
22
+ project: project
23
+ }
24
+ end
25
+ end
26
+
27
+ def fetch_statuses
28
+ statuses = @client.Status.all
29
+
30
+ statuses.map do |status|
31
+ Durt::Status
32
+ .find_or_create_by(source_id: status.id, source: 'Jira') do |s|
33
+ s.name = status.name
34
+ s.active = false
35
+ end
36
+ end
37
+ end
38
+
39
+ # def estimate(key, estimation)
40
+ # comment(key, "Total seconds estimated for this task: #{estimation}")
41
+ # end
42
+ #
43
+ # def comment(key, content)
44
+ # issue = client.Issue.find(key)
45
+ #
46
+ # comment = issue.comments.build
47
+ # comment.save(body: content)
48
+ # end
49
+ #
50
+ private
51
+
52
+ def fetch_issues_query
53
+ statuses_query = statuses.active.map { |s| "\"#{s.name}\"" }.join(', ')
54
+ query = "assignee=currentUser() AND status in (#{statuses_query})"
55
+ puts query
56
+ query
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plugin'
4
+
5
+ module Durt
6
+ class JiraPlugin < Plugin
7
+ def self.demo_config
8
+ {
9
+ username: 'example@mail.com',
10
+ password: 'password',
11
+ site: 'http://project.atlassian.net:443/',
12
+ context_path: '',
13
+ auth_type: :basic
14
+ }
15
+ end
16
+
17
+ def filter(_value)
18
+ bug_tracker.fetch_statuses
19
+
20
+ message = 'Select the statuses that you want to include:'
21
+ chosen_statuses = prompt.multi_select(message, statuses.to_choice_h)
22
+
23
+ statuses.update_all(active: false)
24
+ statuses.where(id: chosen_statuses).update_all(active: true)
25
+ end
26
+
27
+ def bug_tracker_class
28
+ Durt::JiraBugTracker
29
+ end
30
+
31
+ def statuses
32
+ bug_tracker.statuses
33
+ end
34
+
35
+ def prompt
36
+ TTY::Prompt.new
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'bug_tracker'
4
+
5
+ module Durt
6
+ class LocalBugTracker < BugTracker; end
7
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plugin'
4
+
5
+ module Durt
6
+ class LocalPlugin < Plugin
7
+ def bug_tracker_class
8
+ Durt::LocalBugTracker
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'plugin'
4
+
5
+ module Durt
6
+ class NotifyPlugin < Plugin
7
+ def start(value)
8
+ time_tracker.start(value)
9
+ end
10
+
11
+ def stop(value)
12
+ time_tracker.stop(value)
13
+ end
14
+
15
+ def time_tracker_class
16
+ Durt::NotifyTracker
17
+ end
18
+ end
19
+ end