morale 0.1.0

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,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