morale 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require 'morale/flow'
2
+ require 'morale/credentials_store'
3
+
4
+ module Morale
5
+ class Account
6
+ include Morale::Platform
7
+
8
+ class << self
9
+ include Morale::CredentialsStore
10
+ include Morale::Flow
11
+
12
+ def subdomain
13
+ if @subdomain.nil?
14
+ get_credentials
15
+ @subdomain = @credentials[0]
16
+ end
17
+ @subdomain
18
+ end
19
+
20
+ def subdomain=(value)
21
+ @subdomain = value
22
+ @credentials ||= []
23
+ @credentials[0] = value
24
+ write_credentials
25
+ end
26
+
27
+ def api_key
28
+ get_credentials
29
+ @credentials[1]
30
+ end
31
+
32
+ def project
33
+ @credentials[2] if !@credentials.nil? && @credentials.length > 2
34
+ end
35
+
36
+ def project=(value)
37
+ @credentials ||= Array.new(3)
38
+ @credentials[2] = value
39
+ write_credentials
40
+ end
41
+
42
+ def retry_login?
43
+ @login_attempts ||= 0
44
+ @login_attempts += 1
45
+ @login_attempts < 3
46
+ end
47
+
48
+ def get_credentials
49
+ return if @credentials
50
+ unless @credentials = read_credentials
51
+ ask_for_and_save_credentials
52
+ end
53
+ @credentials
54
+ end
55
+
56
+ private
57
+
58
+ def ask_for_and_save_credentials
59
+ @credentials = ask_for_credentials
60
+ write_credentials
61
+ end
62
+
63
+ def ask_for_credentials
64
+ user ||= nil
65
+
66
+ begin
67
+ user = ask_for_subdomain if @subdomain.nil?
68
+ rescue Morale::Client::NotFound
69
+ say "Email is not registered."
70
+ return
71
+ end
72
+
73
+ say "Sign in to Morale."
74
+
75
+ if user.nil?
76
+ say "Email: "
77
+ user = ask
78
+ end
79
+
80
+ say "Password: "
81
+ password = running_on_windows? ? ask_for_secret_on_windows : ask_for_secret
82
+ api_key = Morale::Client.authorize(user, password, @subdomain).api_key
83
+
84
+ say "Invalid email/password combination or API key was not generated." if api_key.nil?
85
+
86
+ [@subdomain, api_key]
87
+ end
88
+
89
+ def ask_for_subdomain
90
+ say "No account specified for Morale."
91
+
92
+ say "Email: "
93
+ user = ask
94
+
95
+ accounts = Morale::Client.accounts user
96
+ account = nil
97
+
98
+ #TODO: This is the same as the account.list --change
99
+ retryable(:indefinate => true) do
100
+ accounts.sort{|a,b| a['account']['group_name'] <=> b['account']['group_name']}.each_with_index do |record, i|
101
+ say "#{i += 1}. #{record['account']['group_name']}"
102
+ end
103
+
104
+ say "Choose an account: "
105
+ index = ask
106
+ account = accounts[index.to_i - 1]
107
+
108
+ if account.nil?
109
+ say "Invalid account."
110
+ raise Exception
111
+ end
112
+ end
113
+
114
+ @subdomain = account['account']['site_address'] unless account.nil?
115
+ user
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,20 @@
1
+ require 'morale/account'
2
+
3
+ module Morale
4
+ class Authorization
5
+ class << self
6
+ def client
7
+ Morale::Client.new(Morale::Account.subdomain, Morale::Account.api_key)
8
+ end
9
+
10
+ def login
11
+ Morale::Account.delete_credentials
12
+ Morale::Account.get_credentials
13
+ end
14
+
15
+ def retry_login?
16
+ Morale::Account.retry_login?
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,76 @@
1
+ require 'httparty'
2
+ require "json"
3
+
4
+ module Morale
5
+ class Client
6
+ class Unauthorized < RuntimeError; end
7
+ class NotFound < RuntimeError; end
8
+
9
+ include HTTParty
10
+ format :json
11
+
12
+ API_VERSION = 'v1'
13
+
14
+ attr_accessor :api_key
15
+ attr_reader :subdomain
16
+
17
+ def self.authorize(user, password, subdomain)
18
+ client = new(subdomain)
19
+ client.unauthorize
20
+ client.api_key = client.class.post('/in', :body => { :email => user, :password => password })["api_key"]
21
+ return client
22
+ end
23
+
24
+ def self.accounts(user)
25
+ client = new
26
+ client.unauthorize
27
+ response = client.class.get("/accounts", :query => { :email => user })
28
+ raise Unauthorized if response.code == 401
29
+ raise NotFound if response.code == 404
30
+ response
31
+ end
32
+
33
+ def accounts
34
+ authorize
35
+ response = self.class.get("/accounts", :query => { :api_key => @api_key })
36
+ raise Unauthorized if response.code == 401
37
+ raise NotFound if response.code == 404
38
+ response
39
+ end
40
+
41
+ def initialize(subdomain="", api_key="")
42
+ @api_key = api_key
43
+ @subdomain = subdomain
44
+ #TODO: Save the domain in a config file
45
+ self.class.default_options[:base_uri] = HTTParty.normalize_base_uri("#{subdomain}#{"." unless subdomain.nil? || subdomain.empty?}lvh.me:3000/api/#{API_VERSION}")
46
+ end
47
+
48
+ def projects
49
+ authorize
50
+ response = self.class.get('/projects')
51
+ raise Unauthorized if response.code == 401
52
+ raise NotFound if response.code == 404
53
+ response
54
+ end
55
+
56
+ def tickets(options={})
57
+ end
58
+
59
+ def ticket(project_id, command)
60
+ authorize
61
+ response = self.class.post("/projects/#{project_id}/tickets", :body => { :command => command })
62
+ raise Unauthorized if response.code == 401
63
+ raise NotFound if response.code == 404
64
+ response
65
+ end
66
+
67
+ def authorize
68
+ self.class.basic_auth @subdomain, @api_key
69
+ end
70
+
71
+ def unauthorize
72
+ self.class.basic_auth nil, nil
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,70 @@
1
+ require 'rubygems'
2
+ require 'thor'
3
+ require 'morale/commands/account'
4
+ require 'morale/commands/authorization'
5
+ require 'morale/commands/project'
6
+ require 'morale/commands/ticket'
7
+
8
+ module Morale
9
+ class Command < Thor
10
+ include Morale::Platform
11
+
12
+ desc "login", "Signs a user in using their email address and password. Stores the users API key locally to use for access later."
13
+ def login
14
+ Morale::Commands::Authorization.login
15
+ end
16
+
17
+ desc "accounts [EMAIL]", "Gets all the subdomains available for a given email address if provided, else it uses the current api key."
18
+ method_options :change => false
19
+ def accounts(email="")
20
+ Morale::Commands::Account.list email, options.change
21
+ end
22
+
23
+ desc "accounts ID", "Changes the current account to the numeric identifier of the account specified."
24
+ def account(id)
25
+ Morale::Commands::Account.select id
26
+ end
27
+
28
+ desc "projects", "Lists the projects available to the user and the current account."
29
+ method_options :change => false
30
+ def projects
31
+ Morale::Commands::Project.list options.change
32
+ end
33
+
34
+ desc "projects ID", "Changes the current project to the numeric identifier of the project specified."
35
+ def project(id)
36
+ Morale::Commands::Project.select id
37
+ end
38
+
39
+ desc "ticket COMMAND", "Creates, updates, or deletes a ticket based on the command specified."
40
+ method_option :command, :aliases => "-c", :type => :array, :desc => "Specify -c without putting the parameter in a string"
41
+ def ticket(command="")
42
+ if command.empty? && !options[:command].nil?
43
+ command = options[:command].compact.join(" ")
44
+ end
45
+ Morale::Commands::Ticket.command command
46
+ end
47
+
48
+ desc "test", "Testing the ticket output."
49
+ def test
50
+ say "+-----+------+-----------------------------------------------------------------------------------------+----------------+----------+----------------+----------+"
51
+ say "| id | type | title | created by | due date | assigned to | priority |"
52
+ say "+-----+------+-----------------------------------------------------------------------------------------+----------------+----------+----------------+----------+"
53
+ say "| 123 | task | hbsdbjhsdjhfdsjfhsdjfhsdjfhsdjfhsdf | Jimmy P. | 08/08/11 | Robert P. | 1 |"
54
+ say "+-----+------+-----------------------------------------------------------------------------------------+----------------+----------+----------------+----------+"
55
+ end
56
+
57
+ class << self
58
+ def client
59
+ Morale::Authorization.client
60
+ end
61
+ end
62
+
63
+ no_tasks do
64
+ def self.handle_no_task_error(task, has_namespace = $thor_runner) #:nodoc:
65
+ self.new.ticket ARGV.compact.join(" ")
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,61 @@
1
+ require 'morale/account'
2
+ require 'morale/client'
3
+ require 'morale/command'
4
+ require 'morale/authorization'
5
+
6
+ module Morale::Commands
7
+ class Account
8
+ class << self
9
+ include Morale::Platform
10
+
11
+ def list(email="", change=false)
12
+ accounts = Morale::Client.accounts(email) unless email.nil? || email.empty?
13
+
14
+ begin
15
+ accounts = Morale::Command.client.accounts if email.nil? || email.empty?
16
+
17
+ if !accounts.nil?
18
+ accounts.sort{|a,b| a['account']['group_name'] <=> b['account']['group_name']}.each_with_index do |record, i|
19
+ say "#{i += 1}. #{record['account']['group_name']}"
20
+ end
21
+
22
+ if change
23
+ say "Choose an account: "
24
+ index = ask
25
+ account = accounts[index.to_i - 1]
26
+
27
+ if account.nil?
28
+ say "Invalid account."
29
+ end
30
+ Morale::Account.subdomain = account['account']['site_address'] unless account.nil?
31
+ end
32
+ else
33
+ say "There were no accounts found."
34
+ end
35
+ rescue Morale::Client::Unauthorized, Morale::Client::NotFound
36
+ say "Authentication failure"
37
+ Morale::Commands::Authorization.login
38
+ retry if Morale::Authorization.retry_login? && !change
39
+ end
40
+ end
41
+
42
+ def select(id)
43
+ begin
44
+ accounts = Morale::Command.client.accounts
45
+ if !accounts.nil?
46
+ account = accounts[id.to_i - 1]
47
+ if account.nil?
48
+ say "Invalid account."
49
+ end
50
+ Morale::Account.subdomain = account['account']['site_address'] unless account.nil?
51
+ else
52
+ say "There were no accounts found."
53
+ end
54
+ rescue Morale::Client::Unauthorized, Morale::Client::NotFound
55
+ say "Authentication failure"
56
+ Morale::Commands::Authorization.login
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ require 'morale/authorization'
2
+
3
+ module Morale::Commands
4
+ class Authorization
5
+ class << self
6
+
7
+ def login
8
+ Morale::Authorization.login
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,63 @@
1
+ require 'morale/client'
2
+ require 'morale/command'
3
+ require 'morale/authorization'
4
+
5
+ module Morale::Commands
6
+ class Project
7
+ class << self
8
+ include Morale::Platform
9
+
10
+ def list(change=false)
11
+ begin
12
+ projects = Morale::Command.client.projects
13
+ if !projects.nil?
14
+ projects.sort{|a,b| a['project']['name'] <=> b['project']['name']}.each_with_index do |record, i|
15
+ puts "#{i += 1}. #{record['project']['name']}"
16
+ end
17
+
18
+ if change
19
+ say "Choose a project: "
20
+ index = ask
21
+ project = projects[index.to_i - 1]
22
+
23
+ if project.nil?
24
+ say "Invalid project."
25
+ end
26
+ Morale::Account.project = project['project']['id'] unless project.nil?
27
+ end
28
+ else
29
+ say "There were no projects found."
30
+ end
31
+ rescue Morale::Client::Unauthorized
32
+ say "Authentication failure"
33
+ Morale::Commands::Authorization.login
34
+ retry if Morale::Authorization.retry_login?
35
+ rescue Morale::Client::NotFound
36
+ say "Communication failure"
37
+ end
38
+ end
39
+
40
+ def select(id)
41
+ begin
42
+ projects = Morale::Command.client.projects
43
+ if !projects.nil?
44
+ project = projects[id.to_i - 1]
45
+ if project.nil?
46
+ say "Invalid project."
47
+ end
48
+ Morale::Account.project = project['project']['id'] unless project.nil?
49
+ else
50
+ say "There were no projects found."
51
+ end
52
+ rescue Morale::Client::Unauthorized
53
+ say "Authentication failure"
54
+ Morale::Commands::Authorization.login
55
+ retry if Morale::Authorization.retry_login?
56
+ rescue Morale::Client::NotFound
57
+ say "Communication failure"
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,36 @@
1
+ require 'morale/client'
2
+ require 'morale/command'
3
+ require 'morale/authorization'
4
+ require 'hirb'
5
+
6
+ module Morale::Commands
7
+ class Ticket
8
+ class << self
9
+ include Morale::Platform
10
+
11
+ def command(command)
12
+ ticket = Morale::Command.client.ticket Morale::Account.project, command
13
+ print ticket
14
+ end
15
+
16
+ private
17
+
18
+ def print(ticket)
19
+ due_date = Date.parse(ticket['due_date']).strftime("%b. %d") unless ticket['due_date'].nil?
20
+ assigned_to = "#{ticket['assigned_to']['user']['first_name']} #{(ticket['assigned_to']['user']['last_name']).to_s[0,1]}." unless ticket['assigned_to'].nil?
21
+
22
+ say Hirb::Helpers::Table.render [{
23
+ :id => ticket['identifier'],
24
+ :type => ticket['type'],
25
+ :title => ticket['title'],
26
+ :created_by => "#{ticket['created_by']['user']['first_name']} #{(ticket['created_by']['user']['last_name']).to_s[0,1]}.",
27
+ :due_date => due_date,
28
+ :assigned_to => assigned_to,
29
+ :priority => ticket['priority']
30
+ }],
31
+ :fields => [:id, :type, :title, :created_by, :due_date, :assigned_to, :priority],
32
+ :headers => { :created_by => "created by", :due_date => "due date", :assigned_to => "assigned to" }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,41 @@
1
+ require 'morale/platform'
2
+
3
+ module Morale
4
+ module CredentialsStore
5
+ include Morale::Platform
6
+
7
+ attr_accessor :credentials
8
+
9
+ def location
10
+ ENV['CREDENTIALS_LOCATION'] || default_location
11
+ end
12
+
13
+ def default_location
14
+ "#{home_directory}/.morale/credentials"
15
+ end
16
+
17
+ def read_credentials
18
+ File.exists?(location) and File.read(location).split("\n")
19
+ end
20
+
21
+ def write_credentials
22
+ FileUtils.mkdir_p(File.dirname(location))
23
+ f = File.open(location, 'w')
24
+ f.puts self.credentials
25
+ f.close
26
+ set_credentials_permissions
27
+ end
28
+
29
+ def delete_credentials
30
+ FileUtils.rm_f(location)
31
+ @credentials = nil
32
+ end
33
+
34
+ private
35
+
36
+ def set_credentials_permissions
37
+ FileUtils.chmod 0700, File.dirname(location)
38
+ FileUtils.chmod 0600, location
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ module Morale
2
+ module Flow
3
+ def retryable(options = {}, &block)
4
+ opts = { :tries => 1, :indefinate => false, :on => Exception }.merge(options)
5
+
6
+ retry_exception, retries, indefinately = opts[:on], opts[:tries], opts[:indefinate]
7
+
8
+ begin
9
+ return yield
10
+ rescue retry_exception
11
+ retry if indefinately || (retries -= 1) > 0
12
+ end
13
+
14
+ yield
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ module Morale
2
+ module Platform
3
+ def home_directory
4
+ running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
5
+ end
6
+
7
+ def running_on_windows?
8
+ RUBY_PLATFORM =~ /mswin32|mingw32/
9
+ end
10
+
11
+ def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/))
12
+ message = message.to_s
13
+ message = set_color(message, color) if color
14
+
15
+ spaces = ""
16
+
17
+ if force_new_line
18
+ $stdout.puts(spaces + message)
19
+ else
20
+ $stdout.print(spaces + message)
21
+ end
22
+ $stdout.flush
23
+ end
24
+
25
+ def ask
26
+ input = $stdin.gets
27
+ input.strip! unless input.nil?
28
+ end
29
+
30
+ def ask_for_secret_on_windows
31
+ require "Win32API"
32
+ char = nil
33
+ secret = ''
34
+
35
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
36
+ break if char == 10 || char == 13 # received carriage return or newline
37
+ if char == 127 || char == 8 # backspace and delete
38
+ secret.slice!(-1, 1)
39
+ else
40
+ # windows might throw a -1 at us so make sure to handle RangeError
41
+ (secret << char.chr) rescue RangeError
42
+ end
43
+ end
44
+ puts
45
+ return secret
46
+ end
47
+
48
+ def ask_for_secret
49
+ echo_off
50
+ secret = ask
51
+ puts
52
+ echo_on
53
+ return secret
54
+ end
55
+
56
+ def echo_off
57
+ system "stty -echo"
58
+ end
59
+
60
+ def echo_on
61
+ system "stty echo"
62
+ end
63
+ end
64
+ end
data/lib/morale.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Morale
2
+ VERSION = "0.1.0"
3
+ end
4
+
5
+ require 'morale/command'
data/morale.gemspec ADDED
@@ -0,0 +1,70 @@
1
+
2
+ Gem::Specification.new do |s|
3
+ s.specification_version = 2 if s.respond_to? :specification_version=
4
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
5
+ s.rubygems_version = '1.3.5'
6
+
7
+ s.name = 'morale'
8
+ s.version = '0.1.0'
9
+ s.date = '2011-09-29'
10
+ s.rubyforge_project = 'morale'
11
+
12
+ s.summary = "Command line interface to create & manage tickets on Morale."
13
+ s.description = "Client library and command-line tool to manage tickets and control your account on Morale."
14
+
15
+ s.authors = ["Brilliant Fantastic"]
16
+ s.email = 'support@teammorale.com'
17
+ s.homepage = 'http://teammorale.com'
18
+
19
+ s.require_paths = %w[lib]
20
+ s.executables = ["morale"]
21
+
22
+ s.rdoc_options = ["--charset=UTF-8"]
23
+ s.extra_rdoc_files = %w[README.md LICENSE]
24
+
25
+ s.add_dependency('hirb', "~> 0.5.0")
26
+ s.add_dependency('httparty', "~> 0.7.8")
27
+ s.add_dependency('json', "~> 1.4.6")
28
+ s.add_dependency('thor', "~> 0.14.6")
29
+
30
+ #s.add_development_dependency('DEVDEPNAME', [">= 1.1.0", "< 2.0.0"])
31
+
32
+ ## Leave this section as-is. It will be automatically generated from the
33
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
34
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
35
+ # = MANIFEST =
36
+ s.files = %w[
37
+ Gemfile
38
+ Gemfile.lock
39
+ LICENSE
40
+ README.md
41
+ Rakefile
42
+ bin/morale
43
+ features/accounts.feature
44
+ features/login.feature
45
+ features/projects.feature
46
+ features/step_definitions/models.rb
47
+ features/support/env.rb
48
+ features/support/hooks.rb
49
+ features/tickets.feature
50
+ lib/morale.rb
51
+ lib/morale/account.rb
52
+ lib/morale/authorization.rb
53
+ lib/morale/client.rb
54
+ lib/morale/command.rb
55
+ lib/morale/commands/account.rb
56
+ lib/morale/commands/authorization.rb
57
+ lib/morale/commands/project.rb
58
+ lib/morale/commands/ticket.rb
59
+ lib/morale/credentials_store.rb
60
+ lib/morale/flow.rb
61
+ lib/morale/platform.rb
62
+ morale.gemspec
63
+ spec/morale/account_spec.rb
64
+ spec/morale/client_spec.rb
65
+ spec/morale/command_spec.rb
66
+ spec/morale/credentials_store_spec.rb
67
+ spec/spec_helper.rb
68
+ ]
69
+ # = MANIFEST =
70
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+ require 'morale/account'
3
+
4
+ describe Morale::Account do
5
+ describe "#subdomain" do
6
+ it "should store the subdomain in the credentials file" do
7
+ Morale::Account.subdomain = "blah"
8
+ File.read(Morale::Account.location).should =~ /blah/
9
+ end
10
+ end
11
+ end