88miles 1.0.1

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