redpomo-reloaded 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/Dockerfile +5 -0
  3. data/Gemfile +9 -0
  4. data/Gemfile.lock +139 -0
  5. data/Guardfile +23 -0
  6. data/LICENSE +22 -0
  7. data/README.md +176 -0
  8. data/Rakefile +6 -0
  9. data/bin/redpomo +7 -0
  10. data/docker-compose.yml +7 -0
  11. data/lib/redpomo.rb +10 -0
  12. data/lib/redpomo/cli.rb +172 -0
  13. data/lib/redpomo/config.rb +26 -0
  14. data/lib/redpomo/entries_printer.rb +33 -0
  15. data/lib/redpomo/entry.rb +65 -0
  16. data/lib/redpomo/file_cache.rb +44 -0
  17. data/lib/redpomo/fuzzy_converter.rb +68 -0
  18. data/lib/redpomo/issue.rb +38 -0
  19. data/lib/redpomo/null_cache.rb +9 -0
  20. data/lib/redpomo/numeric_ext.rb +30 -0
  21. data/lib/redpomo/task.rb +103 -0
  22. data/lib/redpomo/task_list.rb +61 -0
  23. data/lib/redpomo/templates/config.yml +47 -0
  24. data/lib/redpomo/templates/issue_stub.textile +7 -0
  25. data/lib/redpomo/tracker.rb +175 -0
  26. data/lib/redpomo/ui.rb +73 -0
  27. data/lib/redpomo/version.rb +3 -0
  28. data/redpomo.gemspec +33 -0
  29. data/spec/file_cache_spec.rb +22 -0
  30. data/spec/fixtures/add_results.txt +4 -0
  31. data/spec/fixtures/cassettes/cli_add.yml +102 -0
  32. data/spec/fixtures/cassettes/cli_close.yml +50 -0
  33. data/spec/fixtures/cassettes/cli_pull.yml +1222 -0
  34. data/spec/fixtures/cassettes/cli_push.yml +297 -0
  35. data/spec/fixtures/cassettes/close_issue.yml +50 -0
  36. data/spec/fixtures/cassettes/create_issue.yml +102 -0
  37. data/spec/fixtures/cassettes/issues.yml +449 -0
  38. data/spec/fixtures/cassettes/push_entry.yml +55 -0
  39. data/spec/fixtures/close_results.txt +2 -0
  40. data/spec/fixtures/config.yml +16 -0
  41. data/spec/fixtures/printer_output.txt +16 -0
  42. data/spec/fixtures/proper_timelog.csv +4 -0
  43. data/spec/fixtures/pull_results.txt +20 -0
  44. data/spec/fixtures/tasks.txt +3 -0
  45. data/spec/fixtures/timelog.csv +6 -0
  46. data/spec/integration/add_spec.rb +29 -0
  47. data/spec/integration/init_spec.rb +33 -0
  48. data/spec/lib/redpomo/cli_spec.rb +91 -0
  49. data/spec/lib/redpomo/entry_spec.rb +23 -0
  50. data/spec/lib/redpomo/fuzzy_converter_spec.rb +65 -0
  51. data/spec/lib/redpomo/task_spec.rb +39 -0
  52. data/spec/lib/redpomo/tracker_spec.rb +72 -0
  53. data/spec/spec_helper.rb +28 -0
  54. data/spec/support/cli_helpers.rb +76 -0
  55. data/spec/support/fixtures.rb +24 -0
  56. data/spec/support/ruby_ext.rb +20 -0
  57. data/spec/tmp/REDME.md +0 -0
  58. metadata +296 -0
@@ -0,0 +1,26 @@
1
+ require 'active_support/core_ext/module/attribute_accessors'
2
+ require 'active_support/core_ext/hash'
3
+ require 'yaml'
4
+
5
+ require 'redpomo/file_cache'
6
+ require 'redpomo/null_cache'
7
+
8
+ module Redpomo
9
+ module Config
10
+
11
+ mattr_reader :todo_path, :trackers_data, :cache
12
+
13
+ @@cache = NullCache
14
+
15
+ def self.load_from_yaml(path)
16
+ config_path = File.expand_path(path)
17
+ return unless File.exists?(config_path)
18
+ data = YAML::load_file(config_path)
19
+
20
+ @@todo_path = File.expand_path(data["todo"], File.dirname(config_path))
21
+ @@trackers_data = data["trackers"].symbolize_keys!
22
+ @@cache = data["cache"] ? FileCache : NullCache
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ require 'redpomo/numeric_ext'
2
+
3
+ module Redpomo
4
+ class EntriesPrinter
5
+
6
+ def self.print(entries)
7
+ require 'terminal-table'
8
+ entries.group_by(&:date).each do |date, entries|
9
+ duration = 0
10
+ rows = entries.map do |entry|
11
+ task = entry.to_task
12
+ duration += entry.duration
13
+ [
14
+ task.context,
15
+ task.project,
16
+ task.issue,
17
+ task.text,
18
+ entry.duration.seconds_in_words,
19
+ I18n.l(entry.time, format: "%H:%M"),
20
+ I18n.l(entry.end_time, :format => "%H:%M")
21
+ ]
22
+ end
23
+ puts Terminal::Table.new(
24
+ title: "#{ I18n.l(date, format: "%A %x") } - #{ duration.seconds_in_words }",
25
+ headings: [ "Context", "Project", "Issue #", "Description", "Duration", "From", "To" ],
26
+ rows: rows
27
+ )
28
+ puts
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,65 @@
1
+ require 'csv'
2
+
3
+ module Redpomo
4
+ class Entry
5
+
6
+ def self.load_from_csv(text)
7
+ csv_rows(text).map do |data|
8
+ Entry.new(data[0], DateTime.parse(data[1]), data[2].to_i * 60.0) if data.size >= 3
9
+ end.compact.sort_by { |entry| entry.datetime }
10
+ end
11
+
12
+ def self.csv_rows(text)
13
+ if text.match /^Export data created/
14
+ CSV.parse text.split("\n")[4..-1].join("\n")
15
+ else
16
+ CSV.parse text
17
+ end
18
+ end
19
+
20
+ attr_reader :text, :datetime, :duration
21
+
22
+ def initialize(text, datetime, duration)
23
+ @text = text
24
+ @datetime = datetime
25
+ @duration = duration
26
+ end
27
+
28
+ def date
29
+ datetime.to_date
30
+ end
31
+
32
+ def time
33
+ datetime.to_time
34
+ end
35
+
36
+ def end_time
37
+ time + duration
38
+ end
39
+
40
+ def same_date?(entry)
41
+ date == entry.date
42
+ end
43
+
44
+ def same_text?(entry)
45
+ text == entry.text
46
+ end
47
+
48
+ def to_task
49
+ Task.new(nil, text)
50
+ end
51
+
52
+ def push!
53
+ tracker.push_entry!(self) if pushable?
54
+ end
55
+
56
+ def pushable?
57
+ tracker.present? && tracker.pushable_entry?(self)
58
+ end
59
+
60
+ def tracker
61
+ to_task.tracker
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ require 'singleton'
2
+ require 'redpomo/config'
3
+
4
+ module Redpomo
5
+ class FileCache
6
+
7
+ include Singleton
8
+ attr_accessor :cache_path
9
+
10
+ def self.get(key, &block)
11
+ instance.get(key, &block)
12
+ end
13
+
14
+ def get(key, &block)
15
+ return existing_keys[key] if existing_keys.has_key?(key)
16
+ if block_given?
17
+ value = block.call
18
+ set(key, value)
19
+ value
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def initialize
26
+ @cache_path = File.expand_path("~/.redpomo-cache~")
27
+ end
28
+
29
+ def existing_keys
30
+ if File.exists?(cache_path)
31
+ YAML::load_file(cache_path) || {}
32
+ else
33
+ {}
34
+ end
35
+ end
36
+
37
+ def set(key, val)
38
+ dict = existing_keys
39
+ dict[key] = val
40
+ File.open(cache_path, 'w') { |f| f.write(dict.to_yaml) }
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ require 'active_support/core_ext/numeric/time'
2
+ require 'redpomo/entry'
3
+
4
+ module Redpomo
5
+ class FuzzyConverter
6
+
7
+ def self.convert(entries)
8
+ past_entry = nil
9
+ consecutive_entries = []
10
+
11
+ entries << nil
12
+
13
+ result = []
14
+
15
+ entries.each do |entry|
16
+ if past_entry.present?
17
+ if entry.present? && entry.same_date?(past_entry) && entry.same_text?(past_entry)
18
+ consecutive_entries << entry
19
+ else
20
+ result << self.fuzzy_entry(consecutive_entries, entry)
21
+ past_entry = entry
22
+ consecutive_entries = [ entry ]
23
+ end
24
+ else
25
+ past_entry = entry
26
+ consecutive_entries = [ entry ]
27
+ end
28
+ end
29
+
30
+ result
31
+ end
32
+
33
+ def self.fuzzy_entry(entries, next_entry)
34
+ first_entry = entries.first
35
+ last_entry = entries.last
36
+ total_duration = first_entry.time - last_entry.end_time
37
+ duration_till_next_entry = next_entry.time - last_entry.end_time if next_entry.present?
38
+
39
+ entry = if next_entry.nil? || duration_till_next_entry > 1.5 * 3600.0
40
+ duration = last_entry.end_time - first_entry.time
41
+ duration += 1.5 * 3600.0 if next_entry.present? && next_entry.same_date?(last_entry)
42
+ Entry.new(first_entry.text, first_entry.datetime, duration)
43
+ else
44
+ duration = next_entry.time - first_entry.time
45
+ entry = Entry.new(first_entry.text, first_entry.datetime, duration)
46
+ end
47
+ entry
48
+ end
49
+
50
+ # def strip!
51
+ # end_working_day = Time.new(time.year, time.month, time.day, 18, 30, 0, "+00:00")
52
+ # start_launch_time = Time.new(time.year, time.month, time.day, 13, 15, 0, "+00:00")
53
+ # end_launch_time = Time.new(time.year, time.month, time.day, 14, 15, 0, "+00:00")
54
+ # late_night_day = Time.new(time.year, time.month, time.day, 23, 59, 0, "+00:00")
55
+
56
+ # if time < end_working_day && end_time > end_working_day
57
+ # @duration = end_working_day - time
58
+ # elsif time < start_launch_time && end_time > end_launch_time
59
+ # @duration -= 3600.0
60
+ # elsif time < start_launch_time && end_time > start_launch_time
61
+ # @duration = start_launch_time - time
62
+ # elsif time > end_working_day && end_time > late_night_day
63
+ # @duration = late_night_day - time
64
+ # end
65
+ # end
66
+
67
+ end
68
+ end
@@ -0,0 +1,38 @@
1
+ module Redpomo
2
+ class Issue
3
+
4
+ attr_accessor :subject, :description
5
+ attr_accessor :issue_id, :project_id, :tracker
6
+ attr_accessor :due_date, :priority_id
7
+
8
+ def initialize(tracker, data = {})
9
+ @subject = data["subject"]
10
+ @issue_id = data["id"]
11
+ @project_id = data["project_id"]
12
+ @priority_id = data["priority"]["id"] if data["priority"].present?
13
+ @due_date = Date.parse(data["due_date"]) if data["due_date"].present?
14
+ @tracker = tracker
15
+ end
16
+
17
+ def to_task
18
+ label = []
19
+ if @priority_id.present?
20
+ if priority = @tracker.todo_priority(@priority_id)
21
+ label << priority
22
+ end
23
+ end
24
+ label << @due_date.strftime("%Y-%m-%d") if @due_date.present?
25
+ label << @subject
26
+ label << "##{issue_id}"
27
+ label << "+#{project_id}"
28
+ label << "@#{tracker.name}"
29
+ Task.new(nil, label.join(" "))
30
+ end
31
+
32
+ def create!
33
+ data = tracker.create_issue!(self)
34
+ @issue_id = data["issue"]["id"]
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ module Redpomo
2
+ class NullCache
3
+
4
+ def self.get(key, &block)
5
+ block.call
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ class Numeric
2
+ module Units
3
+ Sec = 1
4
+ Min = Sec * 60
5
+ Hour = Min * 60
6
+ Day = Hour * 24
7
+ Week = Day * 7
8
+ Month = Week * 4
9
+ Year = Day * 365
10
+ Decade = Year * 10
11
+ Century = Decade * 10
12
+ Millennium = Century * 10
13
+ Eon = 1.0/0
14
+ end
15
+
16
+ def seconds_in_words
17
+ return "0 secs" if self.zero?
18
+ unit = get_unit(self)
19
+ unit_difference = self / Units.const_get(unit.capitalize)
20
+ unit = unit.to_s.downcase + ('s' if self > 1)
21
+ "#{unit_difference.to_i} #{unit}"
22
+ end
23
+
24
+ private
25
+ def get_unit(time_difference)
26
+ Units.constants.each_cons(2) do |con|
27
+ return con.first if (Units.const_get(con[0])...Units.const_get(con[1])) === time_difference
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,103 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'todo'
3
+ require 'redpomo/tracker'
4
+
5
+ module Todo
6
+ class Task
7
+
8
+ def self.projects_regex
9
+ /(?:\s+|^)\+[\w\-]+/
10
+ end
11
+
12
+ end
13
+ end
14
+
15
+ module Redpomo
16
+ class Task
17
+
18
+ delegate :orig, to: :@task
19
+ delegate :priority, to: :@task
20
+ delegate :date, to: :@task
21
+
22
+ ISSUES_REGEXP = /(?:\s+|^)#[0-9]+/
23
+
24
+ def initialize(list, text)
25
+ @task = Todo::Task.new(text)
26
+ @list = list
27
+ end
28
+
29
+ def context
30
+ @task.contexts.map do |context|
31
+ context.gsub /^@/, ''
32
+ end.first
33
+ end
34
+
35
+ def project
36
+ @task.projects.map do |context|
37
+ context.gsub /^\+/, ''
38
+ end.first
39
+ end
40
+
41
+ def issue
42
+ orig.scan(ISSUES_REGEXP).map(&:strip).map do |issue|
43
+ issue.gsub(/^#/, '').to_i
44
+ end.first
45
+ end
46
+
47
+ def text
48
+ @task.text.gsub(ISSUES_REGEXP, '').strip
49
+ end
50
+
51
+ def close_issue!(message = nil)
52
+ tracker.close_issue!(issue, message)
53
+ end
54
+
55
+ def done!
56
+ @list.remove!(self)
57
+ end
58
+
59
+ def add!
60
+ TaskList.add!(self)
61
+ end
62
+
63
+ def open_in_browser!
64
+ require 'launchy'
65
+ Launchy.open(url)
66
+ end
67
+
68
+ def start_pomodoro!
69
+ require 'applescript'
70
+ command = 'tell application "Pomodoro" to start "'
71
+ command << orig
72
+ command << '"'
73
+ AppleScript.execute(command)
74
+ end
75
+
76
+ def url
77
+ return nil unless tracker.present?
78
+ if issue.present?
79
+ "#{tracker.base_url}/issues/#{issue}"
80
+ elsif project.present?
81
+ "#{tracker.base_url}/projects/#{project}"
82
+ else
83
+ "#{tracker.base_url}/projects/#{tracker.default_project}"
84
+ end
85
+ end
86
+
87
+ def tracker
88
+ Tracker.find(context) if context.present?
89
+ end
90
+
91
+ def to_issue
92
+ issue = Issue.new(tracker)
93
+ issue.subject = text
94
+ issue.project_id = project
95
+ issue.due_date = date
96
+ issue.priority_id = tracker.issue_priority_id(priority) if tracker.present?
97
+ issue
98
+ end
99
+
100
+ end
101
+ end
102
+
103
+
@@ -0,0 +1,61 @@
1
+ require 'todo'
2
+ require 'redpomo/task'
3
+
4
+ module Redpomo
5
+ class TaskList < Array
6
+
7
+ def self.find(task_number)
8
+ list = TaskList.new(Config.todo_path)
9
+ list.find(task_number)
10
+ end
11
+
12
+ def self.pull_from_trackers!
13
+ list = TaskList.new(Config.todo_path)
14
+ list.pull_from_trackers!
15
+ end
16
+
17
+ def self.add!(task)
18
+ list = TaskList.new(Config.todo_path)
19
+ list.add!(task)
20
+ end
21
+
22
+ def initialize(path)
23
+ @path = path
24
+ File.read(path).split("\n").each do |line|
25
+ push Task.new(self, line)
26
+ end
27
+ end
28
+
29
+ def find(task_number)
30
+ slice(task_number.to_i - 1)
31
+ end
32
+
33
+ def remove!(task)
34
+ delete(task)
35
+ write!
36
+ end
37
+
38
+ def add!(task)
39
+ push task
40
+ write!
41
+ end
42
+
43
+ def pull_from_trackers!
44
+ issue_tasks = Tracker.all.map(&:issues).flatten.map(&:to_task)
45
+ delete_if do |task|
46
+ task.tracker.present?
47
+ end
48
+ self << issue_tasks
49
+ self.flatten!
50
+ write!
51
+ Redpomo.ui.info "Pulled #{issue_tasks.count} issues."
52
+ end
53
+
54
+ def write!
55
+ File.open(@path, 'w') do |file|
56
+ file.write map(&:orig).join("\n") + "\n"
57
+ end
58
+ end
59
+
60
+ end
61
+ end