88miles 1.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.
@@ -0,0 +1,107 @@
1
+ module Gigawatt
2
+ module Commands
3
+ class Setup
4
+ attr_accessor :options
5
+
6
+ def self.run!(settings)
7
+ options = Trollop::options do
8
+ banner <<-EOS
9
+ 88 Miles Command line application - http://88miles.net
10
+
11
+ This command will request an access token, giving the command line utility access to your 88 Miles account.
12
+
13
+ To do this, you will be asked for your 88 Miles login and password. Please note that the login and password will not be saved.
14
+
15
+ Usage
16
+ 88miles setup [options]
17
+
18
+ options:
19
+ EOS
20
+ opt :force, "Override existing settings", :default => false, :type => :boolean
21
+ end
22
+
23
+ instance = self.new(settings, options)
24
+
25
+ if instance.settings_exists?
26
+ puts "The settings file #{instance.settings.path} already exists. Use --force to overwrite"
27
+ return SETTINGS_FILE_EXISTS
28
+ end
29
+
30
+ instance.preamble
31
+
32
+ begin
33
+ instance.authenticate
34
+ rescue OAuth2::Error => e
35
+ puts "There was an issue authenticating your account. Please try again."
36
+ return INVALID_OAUTH_TOKEN_EXIT_CODE
37
+ end
38
+ instance.postamble
39
+
40
+ return OK_EXIT_CODE
41
+ end
42
+
43
+ def initialize(settings, options)
44
+ @options = options
45
+ @settings = settings
46
+ end
47
+
48
+ def settings
49
+ @settings
50
+ end
51
+
52
+ def settings_exists?
53
+ return false if options[:force]
54
+ @settings.setup?
55
+ end
56
+
57
+ def preamble
58
+ say("88 Miles command line utility setup")
59
+ say("-----------------------------------")
60
+ say("To setup the 88 Miles command line utility, we need to authenticate you and request an access token.")
61
+ say("We will open a browser, where you will be asked to login and approve access to this app.")
62
+ end
63
+
64
+ def postamble
65
+ say("Thank you. We can now access your account. You can now initialize a directory by running <%= color('88miles init [directory]', BOLD) %>")
66
+ end
67
+
68
+ def get_access_key(url)
69
+ uri = URI(url)
70
+ token = uri.fragment.split('&').map{ |kv| kv.split('=') }.delete_if{ |kv| kv[0] != 'access_token' }.first
71
+ return token[1] if token
72
+ return nil
73
+ end
74
+
75
+ def authenticate
76
+ client = Gigawatt::OAuth.client
77
+
78
+ redirect_uri = Gigawatt::OAuth.redirect_uri
79
+ url = client.auth_code.authorize_url(:response_type => 'token', :redirect_uri => redirect_uri)
80
+
81
+ Launchy.open(url) do |exception|
82
+ say "Couldn't open a browser. Please paste the following URL into a browser"
83
+ say url
84
+ end
85
+
86
+ say("After you have completed the approval process, cut and paste the URL you are redirected to.")
87
+ access_key = get_access_key(ask("URL: ") do |url|
88
+ url.validate = /\A#{redirect_uri}#access_token=.+&state=\Z/
89
+ url.responses[:not_valid] = "That URL doesn't look right. It should look like: #{redirect_uri}#access_token=[some characters]&state="
90
+ end)
91
+
92
+ @settings.access_key = access_key
93
+ @access_key = OAuth.token(access_key)
94
+
95
+ cache = Gigawatt::Cache.new(@settings, @access_key)
96
+
97
+ cache.refresh!
98
+
99
+ @settings.companies = cache.companies
100
+ @settings.projects = cache.projects
101
+
102
+ @settings.write(:accesskey)
103
+ @access_key.token
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,75 @@
1
+ module Gigawatt
2
+ module Commands
3
+ class Start
4
+ attr_accessor :options
5
+
6
+ def self.run!(settings)
7
+ options = Trollop::options do
8
+ banner <<-EOS
9
+ 88 Miles Command line application - http://88miles.net
10
+
11
+ Punch into the project.
12
+
13
+ Usage
14
+ 88miles start [options]
15
+
16
+ options:
17
+ EOS
18
+
19
+
20
+ opt :activity, "Select an activity", :type => :flag
21
+ end
22
+
23
+ instance = self.new(settings, options)
24
+ begin
25
+ return instance.punch_in
26
+ rescue OAuth2::Error => e
27
+ say "Access to your 88 Miles may have been revoked. Please run <%= color('88miles setup', BOLD) %> again."
28
+ return INVALID_OAUTH_TOKEN_EXIT_CODE
29
+ end
30
+ end
31
+
32
+ def initialize(settings, options)
33
+ @settings = settings
34
+ @options = options
35
+
36
+ @access_key = OAuth.token(@settings.access_key)
37
+ @cache = Cache.new(settings, @access_key)
38
+ end
39
+
40
+ def select_activity(project)
41
+ return nil unless options[:activity]
42
+ return nil unless project["activities"]
43
+
44
+ selected = nil
45
+ choose do |menu|
46
+ menu.prompt = "Pick an Activity"
47
+ project["activities"].each do |activity|
48
+ menu.choice("#{activity["name"]}") { selected = activity }
49
+ end
50
+ end
51
+ selected
52
+ end
53
+
54
+ def punch_in
55
+ project = Gigawatt::ProjectFile.new.project
56
+
57
+ if project
58
+ activity = select_activity(project)
59
+ opts = {}
60
+ opts = { :body => { :activity_uuid => activity["uuid"] } } if activity
61
+ response = JSON.parse(@access_key.post("/api/1/projects/#{project["uuid"]}/punch_in.json", opts).body)
62
+ current = response["response"]
63
+ ProjectFile.write(current)
64
+
65
+ company = @cache.companies(true)[project["company_uuid"]]
66
+ say("Punched in to #{company["name"]}: #{project["name"]}")
67
+ return OK_EXIT_CODE
68
+ else
69
+ say("No project found. Did you remember to run <%= color('88miles init [directory]', BOLD) %>?")
70
+ return NO_PROJECT_EXIT_CODE
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,101 @@
1
+ module Gigawatt
2
+ module Commands
3
+ class Status
4
+ attr_accessor :options
5
+
6
+ def self.run!(settings)
7
+ options = Trollop::options do
8
+ banner <<-EOS
9
+ 88 Miles Command line application - http://88miles.net
10
+
11
+ Get status about the linked project
12
+
13
+ Usage:
14
+ 88miles status [options]
15
+
16
+ options:
17
+ EOS
18
+
19
+ opt :sync, "Sync the data from the server first. Uses the cache if false", :type => :flag
20
+ opt :foreground, "Don't exit - just refresh the timer", :type => :flag
21
+ opt :time, "Only return the time", :type => :flag
22
+ end
23
+
24
+ instance = self.new(settings, options)
25
+ return instance.get_settings
26
+ end
27
+
28
+ def initialize(settings, options)
29
+ @settings = settings
30
+ @options = options
31
+
32
+ @access_key = OAuth.token(@settings.access_key)
33
+ @cache = Cache.new(settings, @access_key)
34
+
35
+ @project = Gigawatt::ProjectFile.new.project
36
+ end
37
+
38
+ def get_settings
39
+ unless @project
40
+ say("No project found.")
41
+ return NO_PROJECT_EXIT_CODE
42
+ end
43
+
44
+ if @options[:sync]
45
+ sync = Gigawatt::Commands::Sync.new(@settings, @options)
46
+ begin
47
+ sync.sync
48
+ sync.sync_current
49
+ rescue OAuth2::Error => e
50
+ say "Access to your 88 Miles may have been revoked. Please run <%= color('88miles setup', BOLD) %> again."
51
+ return INVALID_OAUTH_TOKEN_EXIT_CODE
52
+ end
53
+ end
54
+
55
+ if @options[:foreground]
56
+ while(1)
57
+ print_status
58
+ sleep(1)
59
+ end
60
+ print "\n"
61
+ else
62
+ print_status
63
+ print "\n"
64
+ end
65
+
66
+ return OK_EXIT_CODE
67
+ end
68
+
69
+ def print_status
70
+ company = @cache.companies(true)[@project["company_uuid"]]
71
+
72
+ grand_total = @project["grand_total"]
73
+ grand_total += (Time.now.to_i - @project["started_at"]) if @project["running"]
74
+ overdue = grand_total > @project["time_limit"] if @project["time_limit"]
75
+
76
+ clock_string = to_clock_s(grand_total, true)
77
+ clock_string = " [#{HighLine::String.new(clock_string).red}]" if overdue
78
+
79
+ str = ""
80
+ if @options[:time]
81
+ str = clock_string
82
+ else
83
+ str += "#{company["name"]}: #{@project["name"]}"
84
+ str += " [#{clock_string}]"
85
+ str += " #{HighLine::String.new("Running").green}" if @project["running"]
86
+ end
87
+
88
+ print "\e[0K\r#{str}" if @options[:foreground]
89
+ print str unless @options[:foreground]
90
+ end
91
+
92
+ def to_clock_s(time, show_seconds = false)
93
+ hour = (time.abs / 3600).floor
94
+ minute = (time.abs / 60 % 60).floor
95
+ seconds = (time.abs % 60).floor if show_seconds
96
+
97
+ return (time != 0 && (time / time.abs) == -1 ? "-" : "") + hour.to_s.rjust(2, '0') + ":" + minute.to_s.rjust(2, '0') + (show_seconds ? ":" + seconds.to_s.rjust(2, '0') : '')
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,57 @@
1
+ module Gigawatt
2
+ module Commands
3
+ class Stop
4
+ attr_accessor :options
5
+
6
+ def self.run!(settings)
7
+ options = Trollop::options do
8
+ banner <<-EOS
9
+ 88 Miles Command line application - http://88miles.net
10
+
11
+ Punch out of the project.
12
+
13
+ Usage
14
+ 88miles stop [options]
15
+
16
+ options:
17
+ EOS
18
+ opt :notes, "Save notes against the shift", :type => :string
19
+ opt :tags, "Allocate tags to the shift", :type => :string
20
+ end
21
+
22
+ instance = self.new(settings, options)
23
+ begin
24
+ return instance.punch_out
25
+ rescue OAuth2::Error => e
26
+ say "Access to your 88 Miles may have been revoked. Please run <%= color('88miles setup', BOLD) %> again."
27
+ return INVALID_OAUTH_TOKEN_EXIT_CODE
28
+ end
29
+ end
30
+
31
+ def initialize(settings, options)
32
+ @settings = settings
33
+ @options = options
34
+
35
+ @access_key = OAuth.token(@settings.access_key)
36
+ @cache = Cache.new(settings, @access_key)
37
+ end
38
+
39
+ def punch_out
40
+ project = Gigawatt::ProjectFile.new.project
41
+
42
+ if project
43
+ response = JSON.parse(@access_key.post("/api/1/projects/#{project["uuid"]}/punch_out.json", { :params => { :notes => options.notes.to_s } }).body)
44
+ current = response["response"]
45
+ ProjectFile.write(current)
46
+
47
+ company = @cache.companies(true)[project["company_uuid"]]
48
+ say("Punched out of #{company["name"]}: #{project["name"]}")
49
+ return OK_EXIT_CODE
50
+ else
51
+ say("No project found. Did you remember to run <%= color('88miles init [directory]', BOLD) %>?")
52
+ return NO_PROJECT_EXIT_CODE
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,53 @@
1
+ module Gigawatt
2
+ module Commands
3
+ class Sync
4
+ attr_accessor :options
5
+
6
+ def self.run!(settings)
7
+ options = Trollop::options do
8
+ banner <<-EOS
9
+ 88 Miles Command line application - http://88miles.net
10
+
11
+ 88 Miles caches your company and project list to speed things up. Run this command if you add, edit or remove companies or projects
12
+
13
+ If run inside a directory with a linked project, the linked project will be updated too
14
+
15
+ Usage
16
+ 88miles sync
17
+ EOS
18
+ end
19
+
20
+ instance = self.new(settings, options)
21
+ begin
22
+ instance.sync
23
+ instance.sync_current
24
+ rescue OAuth2::Error => e
25
+ say "Access to your 88 Miles may have been revoked. Please run <%= color('88miles setup', BOLD) %> again."
26
+ return INVALID_OAUTH_TOKEN_EXIT_CODE
27
+ end
28
+
29
+ return 0
30
+ end
31
+
32
+ def initialize(settings, options)
33
+ @settings = settings
34
+ @options = options
35
+
36
+ @access_key = OAuth.token(@settings.access_key)
37
+ @cache = Cache.new(settings, @access_key)
38
+ end
39
+
40
+ def sync
41
+ @cache.refresh!
42
+ end
43
+
44
+ def sync_current
45
+ project = Gigawatt::ProjectFile.new.project
46
+ if project
47
+ response = JSON.parse(@access_key.get("/api/1/projects/#{project["uuid"]}.json").body)
48
+ ProjectFile.write(response["response"])
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,23 @@
1
+ module Gigawatt
2
+ class OAuth
3
+ def self.client
4
+ if ENV['ENV'] == 'development'
5
+ client = OAuth2::Client.new('NjtVKz6Di3ccJjn2AGwZKhSxYBX4QHPJ5w1LrZOR', nil, :site => 'http://localhost:3000')
6
+ else
7
+ client = OAuth2::Client.new('hhWZrEF6YTlPUKrggiesnjXAFViLa8FBZNNUtr8L', nil, :site => 'https://88miles.net')
8
+ end
9
+ end
10
+
11
+ def self.token(token)
12
+ OAuth2::AccessToken.new(self.client, token)
13
+ end
14
+
15
+ def self.redirect_uri
16
+ if ENV['ENV'] == 'development'
17
+ "http://localhost:3000/oauth/authorized"
18
+ else
19
+ "https://88miles.net/oauth/authorized"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,53 @@
1
+ module Gigawatt
2
+ class Options
3
+ SUB_COMMANDS = %w(setup init start stop sync status log)
4
+
5
+ def self.parse!
6
+ version_string = File.read(File.join(File.dirname(__FILE__), '..', '..', 'VERSION')).strip
7
+
8
+ options = Trollop::options do
9
+ version "88miles #{version_string}"
10
+ stop_on SUB_COMMANDS
11
+ banner <<-EOS
12
+ 88 Miles Command line application - http://88miles.net
13
+
14
+ Usage:
15
+ 88miles [globaloptions] <subcommand> [options]
16
+
17
+ subcommands:
18
+ setup: Link your 88 Miles account to this program
19
+ init: Link a project to a directory
20
+ start: Punch in to the linked project
21
+ stop: Punch out of the linked project
22
+ sync: Refresh the local cache from the server
23
+ status: Show the project timer
24
+ log: Print out the project shifts
25
+
26
+ globaloptions:
27
+ EOS
28
+ opt :settings, "Path to store 88 Miles settings", :default => Settings.defaults[:path], :type => :string
29
+ end
30
+
31
+ settings = Settings.new(options)
32
+ cmd = ARGV.shift.strip
33
+ case cmd
34
+ when "setup"
35
+ Gigawatt::Commands::Setup.run!(settings)
36
+ when "init"
37
+ Gigawatt::Commands::Init.run!(settings)
38
+ when "start"
39
+ Gigawatt::Commands::Start.run!(settings)
40
+ when "stop"
41
+ Gigawatt::Commands::Stop.run!(settings)
42
+ when "sync"
43
+ Gigawatt::Commands::Sync.run!(settings)
44
+ when "status"
45
+ Gigawatt::Commands::Status.run!(settings)
46
+ when "log"
47
+ Gigawatt::Commands::Log.run!(settings)
48
+ else
49
+ Trollop::die "Unknown subcommand #{cmd.inspect}"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,34 @@
1
+ module Gigawatt
2
+ class ProjectFile
3
+ def find_the_dotfile(dir = Dir.pwd)
4
+ file = File.join(dir, ProjectFile.filename)
5
+ if File.exists?(file)
6
+ return File.join(file)
7
+ else
8
+ parts = dir.split(File::SEPARATOR)[0..-2]
9
+ if parts.length == 0
10
+ return nil
11
+ else
12
+ return find_the_dotfile(File.join(parts))
13
+ end
14
+ end
15
+ end
16
+
17
+ def project
18
+ dotfile = find_the_dotfile
19
+ if dotfile
20
+ YAML.load_file(dotfile)
21
+ else
22
+ nil
23
+ end
24
+ end
25
+
26
+ def self.write(project)
27
+ File.write(File.join(Dir.pwd, ProjectFile.filename), project.to_hash.to_yaml)
28
+ end
29
+
30
+ def self.filename
31
+ ".88miles"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ module Gigawatt
2
+ class Settings
3
+ attr_accessor :access_key, :projects, :companies, :staff
4
+
5
+ def initialize(options = {})
6
+ @options = Settings.defaults.merge(options)
7
+ read if setup?
8
+ end
9
+
10
+ def self.defaults
11
+ {
12
+ :path => File.join(Dir.home, '.88miles')
13
+ }
14
+ end
15
+
16
+ def setup?
17
+ File.exists?(path) && File.directory?(path)
18
+ end
19
+
20
+ def path
21
+ @options[:path]
22
+ end
23
+
24
+ def read
25
+ self.access_key = YAML.load_file(File.join(path, 'accesskey')) if File.exists?(File.join(path, 'accesskey'))
26
+ self.companies = YAML.load_file(File.join(path, 'companies')) if File.exists?(File.join(path, 'companies'))
27
+ self.projects = YAML.load_file(File.join(path, 'projects')) if File.exists?(File.join(path, 'projects'))
28
+ self.staff = YAML.load_file(File.join(path, 'staff')) if File.exists?(File.join(path, 'staff'))
29
+ end
30
+
31
+ def write(type)
32
+ # Make the directory if it doesn't exist
33
+ FileUtils.mkdir_p(path) unless File.exists?(path)
34
+ File.write(File.join(path, 'accesskey'), self.access_key.to_yaml) if type == :accesskey
35
+ File.write(File.join(path, 'companies'), self.companies.to_yaml) if type == :companies
36
+ File.write(File.join(path, 'projects'), self.projects.to_yaml) if type == :projects
37
+ File.write(File.join(path, 'staff'), self.staff.to_yaml) if type == :staff
38
+ FileUtils.chmod 0700, path
39
+ end
40
+ end
41
+ end
data/lib/gigawatt.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'trollop'
2
+ require 'highline/import'
3
+ require 'oauth2'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'open-uri'
7
+ require 'terminal-table'
8
+ require 'launchy'
9
+
10
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'cache')
11
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'settings')
12
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'project_file')
13
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'application')
14
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'oauth')
15
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'options')
16
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'setup')
17
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'init')
18
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'start')
19
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'stop')
20
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'sync')
21
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'status')
22
+ require File.join(File.dirname(__FILE__), 'gigawatt', 'commands', 'log')
23
+
24
+ module Gigawatt
25
+ NO_PROJECT_EXIT_CODE = 3
26
+ SETTINGS_FILE_EXISTS = 2
27
+ INVALID_OAUTH_TOKEN_EXIT_CODE = 1
28
+ OK_EXIT_CODE = 0
29
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'gigawatt'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestGigawatt < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end