redpomo-reloaded 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
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