redpomo 0.0.1

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