tomatoharvest 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f508e0466cc97b1f3b0d619bdfacbd2344b8cb8f
4
+ data.tar.gz: 3284680bde183e758259ec764f3ed84209931501
5
+ SHA512:
6
+ metadata.gz: 5544110fda87a0bb5b50d50f3a4e3d6e2da532088317ba286c6f4571950f33811d7ff03455a9ed2b2fc2668ca8d65a4eec134e70a39c0dd3e9f1a49e7295d418
7
+ data.tar.gz: 7786c551b13055b700232d439cfe52f5c890199579e5b60f7e6aac41bdb0732b73c2116e11a74d9d30f060e651dc04346d3f93bf20ac5ac0047d0d16293c9beb
data/.gitignore ADDED
@@ -0,0 +1,22 @@
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
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in po.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Sam Reh
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,32 @@
1
+ # TomatoHarvest
2
+ Command line pomodoro timer that logs to Harvest.
3
+
4
+ ## Installation
5
+
6
+ $ gem install tomatoharvest
7
+
8
+ Create a file called ~/.tomaconfig with options:
9
+ ```yaml
10
+ domain: myharvestdomain
11
+ username: username
12
+ password: password
13
+ project: harvest project
14
+ task: harvest task
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ $ toma add "Some Task I Have To Do"
20
+ $ toma list
21
+ $ toma start 1
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/samuelreh/tomatoharvest/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
30
+
31
+ ## Credits
32
+ This application is heavily inspired by https://github.com/visionmedia/pomo.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ * Test Tmux
2
+ * Fix 0 hr bug
3
+ * Refactor
4
+ * Count down?
data/bin/toma ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- mode: ruby -*-
3
+
4
+ require 'tomatoharvest'
5
+ TomatoHarvest::CLI.start
@@ -0,0 +1,11 @@
1
+ require 'tomatoharvest/version'
2
+ require 'tomatoharvest/list'
3
+ require 'tomatoharvest/task'
4
+ require 'tomatoharvest/timer'
5
+ require 'tomatoharvest/cli'
6
+ require 'tomatoharvest/os'
7
+ require 'tomatoharvest/notifier'
8
+ require 'tomatoharvest/config'
9
+ require 'tomatoharvest/time_entry'
10
+ require 'tomatoharvest/pomodoro'
11
+ require 'tomatoharvest/tmux'
@@ -0,0 +1,46 @@
1
+ require 'thor'
2
+
3
+ module TomatoHarvest
4
+ class CLI < ::Thor
5
+ DEFAULT_MINUTES = 25
6
+
7
+ desc "add", "add a task"
8
+ def add(name)
9
+ task = Task.new(name)
10
+ List.add(task)
11
+ say "#{task.name} added with id #{task.id}"
12
+ end
13
+
14
+ desc "list", "list all tasks"
15
+ def list
16
+ list = List.all.map do |task|
17
+ [task.id, task.name]
18
+ end
19
+ list.unshift(['id', 'name'])
20
+
21
+ shell = Thor::Base.shell.new
22
+ shell.print_table(list)
23
+ end
24
+
25
+ desc "start", "start a task"
26
+ def start(id, minutes = DEFAULT_MINUTES)
27
+ task = List.find(id)
28
+ config = Config.load.merge("name" => task.name)
29
+ entry = TimeEntry.build_and_test(config)
30
+
31
+ say "Timer started for #{task.name}"
32
+ Timer.start(task.id, minutes: minutes, time_entry: entry)
33
+ end
34
+
35
+ desc "stop", "stop current timer"
36
+ def stop
37
+ if Timer.stop
38
+ say "Timer stopped"
39
+ else
40
+ say "Timer not running"
41
+ end
42
+ end
43
+
44
+
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ module TomatoHarvest
2
+ class Config
3
+ CONFIG_PATH = File.expand_path("#{ENV['$HOME']}/.tomaconfig")
4
+ LOCAL_CONFIG_PATH = File.join(Dir.pwd, '.tomaconfig')
5
+
6
+ def self.load(options = {})
7
+ if !(File.exists? CONFIG_PATH)
8
+ File.open(CONFIG_PATH, 'w') do |file|
9
+ YAML.dump({}, file)
10
+ end
11
+ end
12
+
13
+ hash = YAML.load_file(CONFIG_PATH)
14
+ if File.exists? LOCAL_CONFIG_PATH
15
+ hash.merge!(YAML.load_file(LOCAL_CONFIG_PATH))
16
+ end
17
+
18
+ hash
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,76 @@
1
+ require 'yaml'
2
+
3
+ module TomatoHarvest
4
+ class List
5
+ PATH = File.expand_path("#{ENV['$HOME']}/.toma")
6
+
7
+ attr_reader :items
8
+
9
+ alias :all :items
10
+
11
+ def self.add(item)
12
+ new.tap do |list|
13
+ list.add(item)
14
+ list.save
15
+ end
16
+ end
17
+
18
+ def self.all
19
+ new.all
20
+ end
21
+
22
+ def self.find(id)
23
+ new.find(id)
24
+ end
25
+
26
+ def initialize
27
+ if File.exists?(PATH)
28
+ @items = load_list
29
+ else
30
+ @items = []
31
+ end
32
+ end
33
+
34
+ def find(id)
35
+ # TODO speed this up with an algo
36
+ all.find do |item|
37
+ item.id == id.to_i
38
+ end
39
+ end
40
+
41
+ def save
42
+ yaml = YAML::dump(@items)
43
+ File.open(PATH, "w+") do |f|
44
+ f.write(yaml)
45
+ end
46
+ end
47
+
48
+ def add(item)
49
+ if last_item = @items.last
50
+ id = last_item.id
51
+ else
52
+ id = 0
53
+ end
54
+ item.id = id + 1
55
+
56
+ @items << item
57
+ end
58
+
59
+ private
60
+
61
+ def load_list
62
+ string = ""
63
+
64
+ # better way to do this?
65
+ File.open(PATH, "r") do |f|
66
+ while line = f.gets
67
+ string += line
68
+ end
69
+ end
70
+
71
+ YAML::load(string)
72
+ end
73
+
74
+ end
75
+ end
76
+
@@ -0,0 +1,15 @@
1
+ require_relative 'notifier/notification_center'
2
+
3
+ module TomatoHarvest
4
+ class Notifier
5
+
6
+ def initialize
7
+ @notifier = TomatoHarvest::Notifier::NotificationCenter.new
8
+ end
9
+
10
+ def notify(message, opts = {})
11
+ @notifier.notify(message, opts)
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ require 'terminal-notifier' if TomatoHarvest::OS.mac?
2
+
3
+ module TomatoHarvest
4
+ class Notifier
5
+ class NotificationCenter
6
+ def notify(message, opts = {})
7
+ title = 'TomatoHarvest'
8
+
9
+ TerminalNotifier.notify message, :title => title, :subtitle => opts[:subtitle]
10
+ end
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require 'rbconfig'
2
+
3
+ module TomatoHarvest
4
+ module OS
5
+ module_function
6
+
7
+ def mac?
8
+ (/mac|darwin/ =~ RbConfig::CONFIG['host_os']) != nil
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ module TomatoHarvest
2
+ class Pomodoro
3
+ attr_accessor :seconds, :finished_at
4
+
5
+ def initialize(seconds, finished_at)
6
+ @seconds = seconds
7
+ @finished_at = finished_at
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,26 @@
1
+ module TomatoHarvest
2
+ class Task
3
+
4
+ attr_reader :name
5
+ attr_accessor :id, :pomodoros
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @pomodoros = []
10
+ end
11
+
12
+ def log_pomodoro(seconds)
13
+ finished_at = DateTime.now
14
+ self.pomodoros << Pomodoro.new(seconds, finished_at)
15
+ end
16
+
17
+ def logged_minutes
18
+ sum = pomodoros.inject(0) do |sum, pomodoro|
19
+ sum + pomodoro.seconds
20
+ end
21
+
22
+ sum / 60.0
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,110 @@
1
+ require 'set'
2
+ require 'harvested'
3
+
4
+ module TomatoHarvest
5
+ class TimeEntry
6
+
7
+ attr_accessor :options
8
+
9
+ def self.build_and_test(options = {})
10
+ required = ["domain", "username", "password", "project", "task", "name"].to_set
11
+ keys = options.keys.to_set
12
+ if required.subset?(keys)
13
+ new(options).tap do |entry|
14
+ entry.test
15
+ end
16
+ end
17
+ end
18
+
19
+ def initialize(options = {})
20
+ self.options = options
21
+ end
22
+
23
+ ##
24
+ # Check that the project and task were found on Harvest
25
+
26
+ def test
27
+ raise "Couldn't find project" unless project
28
+ raise "Couldn't find task type" unless task
29
+ end
30
+
31
+ ##
32
+ # Persist time entry to Harvest tracker
33
+
34
+ def log(seconds)
35
+ hours = seconds_to_hours(seconds)
36
+ options = {
37
+ notes: notes,
38
+ hours: hours,
39
+ spent_at: date,
40
+ project_id: project.id,
41
+ task_id: task.id
42
+ }
43
+
44
+
45
+ existing_entry = time_api.all.find do |entry|
46
+ entry.notes == self.notes
47
+ end
48
+
49
+ if existing_entry
50
+ existing_entry.hours += hours
51
+ existing_entry.spent_at = date
52
+ time_api.update(existing_entry)
53
+ else
54
+ entry = Harvest::TimeEntry.new(options)
55
+ time_api.create(entry)
56
+ end
57
+ end
58
+
59
+ ##
60
+ # Notes to send to tracker
61
+
62
+ def notes
63
+ options["name"]
64
+ end
65
+
66
+ ##
67
+ # Date for task (today), formatted properly for tracker
68
+
69
+ def date
70
+ Date.today
71
+ end
72
+
73
+ ##
74
+ # Convert seconds into hours
75
+
76
+ def seconds_to_hours(seconds)
77
+ minutes = (seconds / 60.0)
78
+ unrounded = (minutes / 60.0)
79
+ (unrounded * 100).ceil / 100.0
80
+ end
81
+
82
+ ##
83
+ # Harvest project with name matching options[:project]
84
+
85
+ def project
86
+ time_api.trackable_projects.find do |project|
87
+ project.name == options["project"]
88
+ end
89
+ end
90
+
91
+ ##
92
+ # Harvest task with name matching options[:task]
93
+
94
+ def task
95
+ project.tasks.find do |task|
96
+ task.name == options["task"]
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def time_api
103
+ @time_api ||= begin
104
+ client = Harvest.client(options["domain"], options["username"], options["password"])
105
+ Harvest::API::Time.new(client.credentials)
106
+ end
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,61 @@
1
+ require 'daemons'
2
+
3
+ module TomatoHarvest
4
+ class Timer
5
+ PID_DIR = '~'
6
+ PID_NAME = '.toma'
7
+
8
+ def self.start(task_id, options = {})
9
+ new(task_id, options).start
10
+ end
11
+
12
+ def self.stop
13
+ if monitor = Daemons::Monitor.find(File.expand_path(DIR), APP_NAME)
14
+ monitor.stop
15
+ true
16
+ end
17
+ end
18
+
19
+ def initialize(task_id, options = {})
20
+ @minutes = options[:minutes]
21
+ @time_entry = options[:time_entry]
22
+ @notifier = Notifier.new
23
+ @list = List.new
24
+ @task = @list.find(task_id)
25
+ @timer = 0
26
+ @tmux = Tmux.new
27
+ end
28
+
29
+ def start
30
+ if Daemons.daemonize(app_name: PID_NAME, dir: PID_DIR, dir_mode: :normal)
31
+ at_exit { save_and_log }
32
+ run_timer
33
+ else
34
+ run_timer
35
+ save_and_log
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def run_timer
42
+ @notifier.notify "Pomodoro started for #{@minutes} minutes", :subtitle => @task.name
43
+
44
+ (@minutes * 60).times do |i|
45
+ sleep 1
46
+ @timer += 1
47
+ @tmux.update(@timer)
48
+ end
49
+ end
50
+
51
+ def save_and_log
52
+ @task.log_pomodoro(@timer)
53
+ @list.save
54
+ @time_entry.log(@timer) if @time_entry
55
+ @notifier.notify "Pomodoro finished", :subtitle => "Pomodoro finished!"
56
+ @tmux.update(0)
57
+ end
58
+
59
+ end
60
+ end
61
+
@@ -0,0 +1,32 @@
1
+ module TomatoHarvest
2
+ class Tmux
3
+
4
+ def update(time)
5
+ write_tmux_time time
6
+ refresh_tmux_status_bar
7
+ end
8
+
9
+ private
10
+
11
+ def tmux_time(time)
12
+ mm, ss = time.divmod(60)
13
+ ss = ss.to_s.rjust(2, "0")
14
+ "#[default]#[fg=green]#{mm}:#{ss}#[default]"
15
+ end
16
+
17
+ def write_tmux_time(time)
18
+ path = File.join(ENV['HOME'],'.tomatmux')
19
+ File.open(path, 'w') do |file|
20
+ file.write tmux_time(time)
21
+ end
22
+ end
23
+
24
+ def refresh_tmux_status_bar
25
+ pid = Process.fork do
26
+ exec "tmux refresh-client -S -t $(tmux list-clients -F '\#{client_tty}')"
27
+ end
28
+ Process.detach(pid)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module TomatoHarvest
2
+ VERSION = "0.0.1"
3
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,81 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+ require 'thor'
3
+ require 'tomatoharvest'
4
+
5
+ require 'webmock/rspec'
6
+ require 'minitest/unit'
7
+
8
+ WebMock.disable_net_connect!(allow_localhost: true)
9
+
10
+ RSpec.configure do |c|
11
+ c.include MiniTest::Assertions
12
+
13
+ #
14
+ # Speed up the timer
15
+ #
16
+ c.before :all do
17
+ class TomatoHarvest::Timer
18
+ def sleep(time)
19
+ super(time/100000)
20
+ end
21
+ end
22
+ end
23
+
24
+ #
25
+ # Stub HTTP requests
26
+ #
27
+ c.before do
28
+ body = {
29
+ projects: [ {
30
+ name: 'Pomdoro',
31
+ id: 1,
32
+ tasks: [
33
+ {
34
+ name: 'Ruby Development',
35
+ id: 1
36
+ }
37
+ ]
38
+ } ],
39
+ day_entries: []
40
+ }
41
+
42
+ stub_request(:get, /https:\/\/user:password@domain.harvestapp.com\/daily\/.*/).
43
+ with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>'Harvestable/2.0.0'}).
44
+ to_return(:status => 200, :body => body.to_json, :headers => {})
45
+
46
+ stub_request(:post, "https://user:password@domain.harvestapp.com/daily/add").
47
+ with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json; charset=utf-8', 'User-Agent'=>'Harvestable/2.0.0'}).
48
+ to_return(:status => 200, :body => "", :headers => {})
49
+ end
50
+
51
+ #
52
+ # Don't daemonize for tests
53
+ # Dont notify the terminal
54
+ #
55
+ c.before do
56
+ Daemons.stub(daemonize: false)
57
+ TerminalNotifier.stub(notify: true)
58
+ TomatoHarvest::Tmux.any_instance.stub(update: true)
59
+ end
60
+
61
+ #
62
+ # Cleanup .toma and .tomaconfig
63
+ #
64
+
65
+ [
66
+ ["TomatoHarvest::Config::CONFIG_PATH", File.expand_path('spec/.tomaconfig')],
67
+ ["TomatoHarvest::Config::LOCAL_CONFIG_PATH", File.expand_path('.tomaconfig')],
68
+ ["TomatoHarvest::List::PATH", File.expand_path('spec/.toma')]
69
+ ].each do |tuple|
70
+ path = tuple[1]
71
+
72
+ c.before :each do
73
+ stub_const(tuple[0], path)
74
+ File.delete(path) if File.exists?(path)
75
+ end
76
+
77
+ c.after :each do
78
+ File.delete(path) if File.exists?(path)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,66 @@
1
+ require 'helper'
2
+
3
+ describe TomatoHarvest::CLI do
4
+
5
+ describe 'add' do
6
+
7
+ it 'adds a task to the list' do
8
+ out = capture_io { TomatoHarvest::CLI.start ['add', 'foo'] }.join ''
9
+ expect(out).to match(/foo added with id 1/)
10
+ end
11
+
12
+ end
13
+
14
+ describe 'list' do
15
+
16
+ it 'shows a table of the tasks' do
17
+ TomatoHarvest::CLI.start ['add', 'foo']
18
+ TomatoHarvest::CLI.start ['add', 'bar']
19
+ out = capture_io { TomatoHarvest::CLI.start ['list'] }.join ''
20
+ expect(out).to match(/id name\n\s*1 foo\n\s*2 bar/)
21
+ end
22
+
23
+ end
24
+
25
+ describe 'start' do
26
+ before do
27
+ TomatoHarvest::CLI.start ['add', 'foo']
28
+ end
29
+
30
+ it 'starts the timer' do
31
+ out = capture_io { TomatoHarvest::CLI.start ['start', 1] }.join ''
32
+ expect(out).to match(/Timer started for foo/)
33
+ end
34
+
35
+ it 'starts the timer with specified length' do
36
+ out = capture_io { TomatoHarvest::CLI.start ['start', 1, 15] }.join ''
37
+ expect(out).to match(/Timer started for foo/)
38
+ end
39
+
40
+ context 'when config has valid harvest options' do
41
+
42
+ before do
43
+ options = {
44
+ project: 'Pomodoro',
45
+ type: 'Ruby Development',
46
+ domain: 'domain',
47
+ username: 'user',
48
+ password: 'password'
49
+ }
50
+
51
+ path = TomatoHarvest::Config::CONFIG_PATH
52
+ File.open(path, 'w') do |file|
53
+ YAML::dump(options, file)
54
+ end
55
+ end
56
+
57
+ it 'starts the timer with specified length' do
58
+ out = capture_io { TomatoHarvest::CLI.start ['start', 1] }.join ''
59
+ expect(out).to match(/Timer started for foo/)
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,44 @@
1
+ require 'helper'
2
+
3
+ describe TomatoHarvest::Config do
4
+
5
+ describe '.load' do
6
+ let(:global_options) do
7
+ {
8
+ project: 'Project',
9
+ type: 'Ruby Development',
10
+ }
11
+ end
12
+
13
+ before do
14
+ File.open(TomatoHarvest::Config::CONFIG_PATH, 'w') do |file|
15
+ YAML::dump(global_options, file)
16
+ end
17
+ end
18
+
19
+ it 'loads from the yaml config file' do
20
+ expect(TomatoHarvest::Config.load).to eql(global_options)
21
+ end
22
+
23
+ context 'when there is a config file in the current dir' do
24
+
25
+ it 'overrides global config' do
26
+ options = {
27
+ type: 'JS Development',
28
+ }
29
+ local_config = File.join(Dir.pwd, '.tomaconfig')
30
+
31
+ File.open(local_config, 'w') do |file|
32
+ YAML::dump(options, file)
33
+ end
34
+
35
+ result = global_options.merge(options)
36
+
37
+ expect(TomatoHarvest::Config.load).to eql(result)
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,51 @@
1
+ require 'helper'
2
+
3
+ describe TomatoHarvest::List do
4
+
5
+ def add_task(name)
6
+ task = TomatoHarvest::Task.new(name)
7
+ TomatoHarvest::List.add(task)
8
+ end
9
+
10
+ describe '.add' do
11
+
12
+ it 'adds to the list' do
13
+ add_task('foo')
14
+ expect(described_class.all.first).to be_an_instance_of(TomatoHarvest::Task)
15
+ end
16
+
17
+ end
18
+
19
+ describe '.list' do
20
+
21
+ it 'should have two items' do
22
+ add_task('foo')
23
+ add_task('bar')
24
+ expect(described_class.all.count).to eql(2)
25
+ end
26
+
27
+ end
28
+
29
+ describe '.find' do
30
+
31
+ it 'returns the task with the corresponding id' do
32
+ add_task('foo')
33
+ add_task('bar')
34
+ expect(described_class.find(1).name).to eql('foo')
35
+ expect(described_class.find(2).name).to eql('bar')
36
+ end
37
+
38
+ end
39
+
40
+ describe '#add' do
41
+
42
+ it 'adds the task to the items array' do
43
+ task = TomatoHarvest::Task.new('foo')
44
+ list = described_class.new
45
+ list.add(task)
46
+ expect(list.items.first.id).to eql(1)
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,106 @@
1
+ require 'helper'
2
+
3
+ describe TomatoHarvest::TimeEntry do
4
+
5
+ describe '#test' do
6
+
7
+ let(:entry) do
8
+ described_class.new.tap do |entry|
9
+ entry.stub(project: double, task: double)
10
+ end
11
+ end
12
+
13
+ it "raises an error if project can't be found" do
14
+ entry.stub(project: nil)
15
+ expect{ entry.test }.to raise_error("Couldn't find project")
16
+ end
17
+
18
+ it "raises an error if task can't be found" do
19
+ entry.stub(task: nil)
20
+ expect{ entry.test }.to raise_error("Couldn't find task type")
21
+ end
22
+
23
+ end
24
+
25
+ describe '#log' do
26
+
27
+ context 'task is already logged today' do
28
+ let(:options) do
29
+ {
30
+ 'domain' => 'domain',
31
+ 'username' => 'user',
32
+ 'password' => 'password',
33
+ 'project' => 'Pomodoro',
34
+ 'task' => 'Ruby Development',
35
+ 'name' => 'Template Refactoring'
36
+ }
37
+ end
38
+
39
+ before do
40
+ body = {
41
+ projects: [ {
42
+ name: 'Pomodoro',
43
+ id: 1,
44
+ tasks: [
45
+ {
46
+ name: 'Ruby Development',
47
+ id: 1
48
+ }
49
+ ]
50
+ } ],
51
+
52
+ day_entries: [ {
53
+ notes: 'Template Refactoring',
54
+ project_id: 1,
55
+ task_id: 1,
56
+ hours: 1
57
+ } ]
58
+ }
59
+
60
+ stub_request(:get, /https:\/\/user:password@domain.harvestapp.com\/daily\/.*/).
61
+ to_return(:status => 200, :body => body.to_json, :headers => {})
62
+ end
63
+
64
+ it 'updates exisiting entry' do
65
+ update_url = "https://user:password@domain.harvestapp.com/daily/update/"
66
+ body = {
67
+ notes: "Template Refactoring",
68
+ project_id: 1,
69
+ task_id: 1,
70
+ hours: 1.5,
71
+ spent_at: Date.today.strftime("%Y-%m-%d")
72
+ }
73
+ stub = stub_request(:put, update_url).with(:body => body.to_json)
74
+
75
+ entry = TomatoHarvest::TimeEntry.new(options)
76
+ entry.log(60 * 30)
77
+
78
+ stub.should have_been_requested
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ describe '#notes' do
86
+
87
+ it 'concats name and description' do
88
+ entry = described_class.new('name' => "Name")
89
+ expect(entry.notes).to eql("Name")
90
+ end
91
+
92
+ end
93
+
94
+ describe '#hours' do
95
+
96
+ it 'converts minutes to hours' do
97
+ expect(described_class.new.seconds_to_hours(60 * 60)).to be(1.00)
98
+ end
99
+
100
+ it 'round to hundreths' do
101
+ expect(described_class.new.seconds_to_hours(61 * 60)).to be(1.02)
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,47 @@
1
+ require 'helper'
2
+
3
+ describe TomatoHarvest::Timer do
4
+
5
+ describe '.start' do
6
+
7
+ let(:task) { TomatoHarvest::Task.new('foo') }
8
+
9
+ before do
10
+ list = TomatoHarvest::List.new
11
+ list.add(task)
12
+ list.save
13
+ end
14
+
15
+ def stub_notifier(minutes)
16
+ message = "Pomodoro started for #{minutes} minutes"
17
+ options = {:title=>"TomatoHarvest", :subtitle=> 'foo'}
18
+ TerminalNotifier.should_receive(:notify).with(message, options)
19
+
20
+ message = "Pomodoro finished"
21
+ options = {:title=>"TomatoHarvest", :subtitle=> 'Pomodoro finished!'}
22
+ TerminalNotifier.should_receive(:notify).with(message, options)
23
+ end
24
+
25
+ it 'can run for a custom length' do
26
+ TomatoHarvest::Timer.start(task.id, minutes: 15)
27
+
28
+ reloaded_task = TomatoHarvest::List.find(task.id)
29
+ expect(reloaded_task.logged_minutes).to eql(15.0)
30
+ end
31
+
32
+ it 'can be run twice' do
33
+ TomatoHarvest::Timer.start(task.id, minutes: 20)
34
+ TomatoHarvest::Timer.start(task.id, minutes: 20)
35
+ reloaded_task = TomatoHarvest::List.find(task.id)
36
+ expect(reloaded_task.logged_minutes).to eql(40.0)
37
+ end
38
+
39
+ it 'logs a time entry if passed in' do
40
+ entry = double
41
+ entry.should_receive(:log)
42
+ TomatoHarvest::Timer.start(task.id, time_entry: entry, minutes: 25)
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tomatoharvest'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tomatoharvest"
8
+ spec.version = TomatoHarvest::VERSION
9
+ spec.authors = ["Sam Reh"]
10
+ spec.email = ["samuelreh@gmail.com"]
11
+ spec.summary = %q{Log your pomodoros to Harvest}
12
+ spec.description = %q{Command line pomodoro timer that logs to Harvest.}
13
+ spec.homepage = "http://github.com/samuelreh/tomatoharvest/"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.6"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "webmock"
25
+
26
+ spec.add_dependency('thor', '~> 0.19')
27
+ spec.add_dependency('harvested')
28
+ spec.add_dependency('daemons')
29
+ spec.add_dependency('terminal-notifier', '~> 1.4') if TomatoHarvest::OS.mac?
30
+ end
metadata ADDED
@@ -0,0 +1,191 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tomatoharvest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sam Reh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: thor
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.19'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.19'
83
+ - !ruby/object:Gem::Dependency
84
+ name: harvested
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: daemons
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: terminal-notifier
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '1.4'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: '1.4'
125
+ description: Command line pomodoro timer that logs to Harvest.
126
+ email:
127
+ - samuelreh@gmail.com
128
+ executables:
129
+ - toma
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - Gemfile
135
+ - LICENSE.txt
136
+ - README.md
137
+ - Rakefile
138
+ - TODO
139
+ - bin/toma
140
+ - lib/tomatoharvest.rb
141
+ - lib/tomatoharvest/cli.rb
142
+ - lib/tomatoharvest/config.rb
143
+ - lib/tomatoharvest/list.rb
144
+ - lib/tomatoharvest/notifier.rb
145
+ - lib/tomatoharvest/notifier/notification_center.rb
146
+ - lib/tomatoharvest/os.rb
147
+ - lib/tomatoharvest/pomodoro.rb
148
+ - lib/tomatoharvest/task.rb
149
+ - lib/tomatoharvest/time_entry.rb
150
+ - lib/tomatoharvest/timer.rb
151
+ - lib/tomatoharvest/tmux.rb
152
+ - lib/tomatoharvest/version.rb
153
+ - spec/helper.rb
154
+ - spec/lib/tomatoharvest/cli_spec.rb
155
+ - spec/lib/tomatoharvest/config_spec.rb
156
+ - spec/lib/tomatoharvest/list_spec.rb
157
+ - spec/lib/tomatoharvest/time_entry_spec.rb
158
+ - spec/lib/tomatoharvest/timer_spec.rb
159
+ - tomatoharvest.gemspec
160
+ homepage: http://github.com/samuelreh/tomatoharvest/
161
+ licenses:
162
+ - MIT
163
+ metadata: {}
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - '>='
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubyforge_project:
180
+ rubygems_version: 2.0.3
181
+ signing_key:
182
+ specification_version: 4
183
+ summary: Log your pomodoros to Harvest
184
+ test_files:
185
+ - spec/helper.rb
186
+ - spec/lib/tomatoharvest/cli_spec.rb
187
+ - spec/lib/tomatoharvest/config_spec.rb
188
+ - spec/lib/tomatoharvest/list_spec.rb
189
+ - spec/lib/tomatoharvest/time_entry_spec.rb
190
+ - spec/lib/tomatoharvest/timer_spec.rb
191
+ has_rdoc: