redpomo 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour --format d
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in redpomo.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'vcr'
8
+ gem 'mocha'
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Stefano Verna
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Redpomo
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'redpomo'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install redpomo
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/redpomo ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'redpomo'
4
+ Redpomo::CLI.start
5
+
@@ -0,0 +1,72 @@
1
+ require 'thor'
2
+
3
+ require 'redpomo/config'
4
+ require 'redpomo/task_list'
5
+ require 'redpomo/entry'
6
+ require 'redpomo/entries_printer'
7
+ require 'redpomo/fuzzy_converter'
8
+
9
+ module Redpomo
10
+ class CLI < ::Thor
11
+
12
+ desc "pull", "imports Redmine open issues into local todo.txt"
13
+ method_option :config, aliases: "-c", default: '~/.redpomo'
14
+ def pull
15
+ configure!
16
+ TaskList.pull_from_trackers!
17
+ end
18
+
19
+ desc "push LOGFILE", "parses Pomodoro export file and imports to Redmine clients"
20
+ method_option :config, aliases: "-c", default: '~/.redpomo'
21
+ method_option :fuzzy, aliases: "-f", type: :boolean
22
+ method_option :dry_run, aliases: "-n", type: :boolean
23
+ def push(path)
24
+ configure!
25
+
26
+ entries = Entry.load_from_csv(path)
27
+ entries = FuzzyConverter.convert(entries) if @options[:fuzzy]
28
+
29
+ unless @options[:dry_run]
30
+ entries.each(&:push!)
31
+ end
32
+
33
+ EntriesPrinter.print(entries)
34
+ end
35
+
36
+ desc "open TASK", "opens up the Redmine issue page of the selected task"
37
+ method_option :config, aliases: "-c", default: '~/.redpomo'
38
+ def open(task_number)
39
+ configure!
40
+
41
+ task = TaskList.find(task_number)
42
+ task.open_in_browser!
43
+ end
44
+
45
+ desc "start TASK", "starts a Pomodoro session for the selected task"
46
+ method_option :config, aliases: "-c", default: '~/.redpomo'
47
+ def start(task_number)
48
+ configure!
49
+
50
+ task = TaskList.find(task_number)
51
+ task.start_pomodoro!
52
+ end
53
+
54
+ desc "close TASK", "marks a todo.txt task as complete, and closes the related Redmine issue"
55
+ method_option :config, aliases: "-c", default: '~/.redpomo'
56
+ method_option :message, aliases: "-m"
57
+ def close(task_number)
58
+ configure!
59
+
60
+ task = TaskList.find(task_number)
61
+ task.done!
62
+ task.close_issue!
63
+ end
64
+
65
+ private
66
+
67
+ def configure!
68
+ Config.load_from_yaml(@options[:config])
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,50 @@
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
+ data = YAML::load_file(File.expand_path(path))
17
+
18
+ @@todo_path = File.expand_path(data["todo"])
19
+ @@trackers_data = data["trackers"].symbolize_keys!
20
+ @@cache = data["cache"] ? FileCache : NullCache
21
+ end
22
+
23
+ # def todo_path
24
+ # File.expand_path(@data["todo"])
25
+ # end
26
+
27
+ # def tasks
28
+ # @tasks ||= Todo::List.new(todo_path)
29
+ # end
30
+
31
+ # def find_task(number)
32
+ # tasks[number.to_i - 1]
33
+ # end
34
+
35
+ # def trackers
36
+ # @trackers ||= @data["trackers"].map do |key, data|
37
+ # Tracker.new(data.merge(name: key))
38
+ # end
39
+ # end
40
+
41
+ # def issues
42
+ # [].tap do |issues|
43
+ # trackers.each do |tracker|
44
+ # issues << tracker.issues
45
+ # end
46
+ # end.flatten
47
+ # end
48
+
49
+ end
50
+ 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,49 @@
1
+ require 'csv'
2
+
3
+ module Redpomo
4
+ class Entry
5
+
6
+ def self.load_from_csv(path)
7
+ CSV.parse(File.read(path).split("\n")[4..-1].join("\n")).map do |data|
8
+ Entry.new(data[0], DateTime.parse(data[1]), data[2].to_i * 60.0)
9
+ end.sort_by { |entry| entry.datetime }
10
+ end
11
+
12
+ attr_reader :text, :datetime, :duration
13
+
14
+ def initialize(text, datetime, duration)
15
+ @text = text
16
+ @datetime = datetime
17
+ @duration = duration
18
+ end
19
+
20
+ def date
21
+ datetime.to_date
22
+ end
23
+
24
+ def time
25
+ datetime.to_time
26
+ end
27
+
28
+ def end_time
29
+ time + duration
30
+ end
31
+
32
+ def same_date?(entry)
33
+ date == entry.date
34
+ end
35
+
36
+ def same_text?(entry)
37
+ text == entry.text
38
+ end
39
+
40
+ def to_task
41
+ Task.new(nil, text)
42
+ end
43
+
44
+ def push!
45
+ to_task.tracker.push_entry!(self)
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,40 @@
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 inizialize
26
+ @cache_path = File.expand_path("~/.redpomo-cache~")
27
+ end
28
+
29
+ def existing_keys
30
+ File.exists?(cache_path) ? (YAML::load_file(cache_path) || {}) : {}
31
+ end
32
+
33
+ def set(key, val)
34
+ dict = existing_keys
35
+ dict[key] = val
36
+ File.open(cache_path, 'w') { |f| f.write(dict.to_yaml) }
37
+ end
38
+
39
+ end
40
+ 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,24 @@
1
+ module Redpomo
2
+ class Issue
3
+
4
+ attr_reader :title, :issue_id, :project, :tracker, :due_date
5
+
6
+ def initialize(tracker, data)
7
+ @title = data["subject"]
8
+ @issue_id = data["id"]
9
+ @project = data["project"]
10
+ @due_date = Date.parse(data["due_date"]) if data["due_date"].present?
11
+ @tracker = tracker
12
+ end
13
+
14
+ def to_task
15
+ label = [ title ]
16
+ label << @due_date.strftime("%Y-%m-%d") if @due_date.present?
17
+ label << "##{issue_id}"
18
+ label << "+#{project}"
19
+ label << "@#{tracker.name}"
20
+ Todo::Task.new(label.join(" "))
21
+ end
22
+
23
+ end
24
+ 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,29 @@
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
+ unit = get_unit(self)
18
+ unit_difference = self / Units.const_get(unit.capitalize)
19
+ unit = unit.to_s.downcase + ('s' if self > 1)
20
+ "#{unit_difference.to_i} #{unit}"
21
+ end
22
+
23
+ private
24
+ def get_unit(time_difference)
25
+ Units.constants.each_cons(2) do |con|
26
+ return con.first if (Units.const_get(con[0])...Units.const_get(con[1])) === time_difference
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ require 'redpomo/config'
2
+
3
+ module Redpomo
4
+ class Puller
5
+
6
+ def initialize(options = {})
7
+ @options = options
8
+ end
9
+
10
+ def execute
11
+ list = Todo::List.new(
12
+ config.issues.map(&:to_task) +
13
+ unrelated_tasks
14
+ )
15
+ list.write_to(config.todo_path)
16
+ end
17
+
18
+ private
19
+
20
+ def config
21
+ @config ||= Redpomo::Config.new(@options[:config])
22
+ end
23
+
24
+ def trackers_contexts
25
+ @trackers_context ||= config.trackers.map(&:context)
26
+ end
27
+
28
+ def unrelated_tasks
29
+ config.tasks.select do |task|
30
+ ! task.include_contexts?(trackers_contexts)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ require 'terminal-table'
2
+
3
+ require 'redpomo/entry'
4
+ require 'redpomo/config'
5
+
6
+ module Redpomo
7
+ class Pusher
8
+
9
+ def initialize(log_path, options = {})
10
+ @options = options
11
+ @log_path = File.expand_path(log_path)
12
+ end
13
+
14
+ def execute
15
+ entries_to_push = @options[:fuzzy] ? fuzzy_entries : entries
16
+
17
+ unless @options[:dry_run]
18
+ entries_to_push.each do |entry|
19
+ tracker = config.tracker_for_task(entry.to_task)
20
+ tracker.push_entry(entry)
21
+ end
22
+ end
23
+
24
+ entries_to_push.group_by(&:date).each do |date, entries|
25
+ duration = 0
26
+ rows = entries.map do |entry|
27
+ task = entry.to_task
28
+ duration += entry.duration
29
+ [ task.contexts.first, task.projects.first, task.issues.first, task.text, entry.duration.seconds_in_words, I18n.l(entry.time, format: "%H:%M"), I18n.l(entry.end_time, :format => "%H:%M") ]
30
+ end
31
+ puts Terminal::Table.new(
32
+ title: "#{ I18n.l(date, format: "%A %x") } - #{ duration.seconds_in_words }",
33
+ headings: [ "Context", "Project", "Issue #", "Description", "Duration", "From", "To" ],
34
+ rows: rows
35
+ )
36
+ puts
37
+ end
38
+
39
+ end
40
+
41
+ private
42
+
43
+ def config
44
+ @config ||= Redpomo::Config.new(@options[:config])
45
+ end
46
+
47
+ def entries
48
+ @entries ||= raw_log.map do |line_data|
49
+ Entry.from_csv(line_data)
50
+ end.sort_by { |entry| entry.datetime }
51
+ end
52
+
53
+ def raw_log
54
+ CSV.parse File.read(@log_path).split("\n")[4..-1].join("\n")
55
+ end
56
+
57
+
58
+ end
59
+ end
@@ -0,0 +1,88 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'todo-txt/task'
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
+
20
+ ISSUES_REGEXP = /(?:\s+|^)#[0-9]+/
21
+
22
+ def initialize(list, text)
23
+ @task = Todo::Task.new(text)
24
+ @list = list
25
+ end
26
+
27
+ def context
28
+ @task.contexts.map do |context|
29
+ context.gsub /^@/, ''
30
+ end.first
31
+ end
32
+
33
+ def project
34
+ @task.projects.map do |context|
35
+ context.gsub /^\+/, ''
36
+ end.first
37
+ end
38
+
39
+ def issue
40
+ orig.scan(ISSUES_REGEXP).map(&:strip).map do |issue|
41
+ issue.gsub(/^#/, '').to_i
42
+ end.first
43
+ end
44
+
45
+ def text
46
+ @task.text.gsub(ISSUES_REGEXP, '').strip
47
+ end
48
+
49
+ def close_issue!(message = nil)
50
+ tracker.close_issue!(issue, message)
51
+ end
52
+
53
+ def done!
54
+ @list.remove!(self)
55
+ end
56
+
57
+ def open_in_browser!
58
+ require 'launchy'
59
+ Launchy.open(url)
60
+ end
61
+
62
+ def start_pomodoro!
63
+ require 'applescript'
64
+ command = 'tell application "Pomodoro" to start "'
65
+ command << orig
66
+ command << '"'
67
+ AppleScript.execute(command)
68
+ end
69
+
70
+ def url
71
+ return nil unless tracker.present?
72
+ if issue.present?
73
+ "#{tracker.base_url}/issues/#{issue}"
74
+ elsif project.present?
75
+ "#{tracker.base_url}/projects/#{project}"
76
+ else
77
+ "#{tracker.base_url}/projects/#{tracker.default_project}"
78
+ end
79
+ end
80
+
81
+ def tracker
82
+ Tracker.find(context)
83
+ end
84
+
85
+ end
86
+ end
87
+
88
+