kanbantastic 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +3 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +42 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +0 -0
  7. data/Rakefile +9 -0
  8. data/kanbantastic.gemspec +35 -0
  9. data/lib/kanbantastic.rb +8 -0
  10. data/lib/kanbantastic/base.rb +140 -0
  11. data/lib/kanbantastic/column.rb +45 -0
  12. data/lib/kanbantastic/config.rb +15 -0
  13. data/lib/kanbantastic/task.rb +130 -0
  14. data/lib/kanbantastic/user.rb +15 -0
  15. data/lib/kanbantastic/version.rb +3 -0
  16. data/spec/cassettes/base/get.yml +166 -0
  17. data/spec/cassettes/base/post.yml +164 -0
  18. data/spec/cassettes/base/put.yml +154 -0
  19. data/spec/cassettes/base/rectify_time.yml +30 -0
  20. data/spec/cassettes/cassette2.yml +63 -0
  21. data/spec/cassettes/cassette3.yml +63 -0
  22. data/spec/cassettes/cassette6.yml +123 -0
  23. data/spec/cassettes/task/all.yml +123 -0
  24. data/spec/cassettes/task/archive1.yml +557 -0
  25. data/spec/cassettes/task/archive2.yml +371 -0
  26. data/spec/cassettes/task/archived1.yml +185 -0
  27. data/spec/cassettes/task/archived2.yml +619 -0
  28. data/spec/cassettes/task/create.yml +123 -0
  29. data/spec/cassettes/task/find.yml +125 -0
  30. data/spec/cassettes/task/move_to_first_column.yml +722 -0
  31. data/spec/cassettes/task/move_to_last_column.yml +846 -0
  32. data/spec/cassettes/task/move_to_next_column.yml +350 -0
  33. data/spec/cassettes/task/move_to_previous_column.yml +412 -0
  34. data/spec/cassettes/task/move_to_second_column.yml +722 -0
  35. data/spec/cassettes/task/owner.yml +185 -0
  36. data/spec/cassettes/task/update.yml +185 -0
  37. data/spec/cassettes/task/update_column_id.yml +247 -0
  38. data/spec/cassettes/workspaces_with_projects.yml +34 -0
  39. data/spec/kanbantastic/base_spec.rb +179 -0
  40. data/spec/kanbantastic/column_spec.rb +83 -0
  41. data/spec/kanbantastic/config_spec.rb +12 -0
  42. data/spec/kanbantastic/kanbanery_spec.rb +16 -0
  43. data/spec/kanbantastic/task_spec.rb +380 -0
  44. data/spec/spec_helper.rb +22 -0
  45. metadata +194 -0
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create ruby-1.9.2-p180@kanbantastic
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in kanbantastic.gemspec
4
+ gemspec
@@ -0,0 +1,42 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ kanbantastic (0.1.1)
5
+ activemodel
6
+ httparty
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ activemodel (3.0.5)
12
+ activesupport (= 3.0.5)
13
+ builder (~> 2.1.2)
14
+ i18n (~> 0.4)
15
+ activesupport (3.0.5)
16
+ builder (2.1.2)
17
+ crack (0.1.8)
18
+ diff-lcs (1.1.2)
19
+ fakeweb (1.3.0)
20
+ httparty (0.7.4)
21
+ crack (= 0.1.8)
22
+ i18n (0.5.0)
23
+ mocha (0.9.12)
24
+ rspec (2.5.0)
25
+ rspec-core (~> 2.5.0)
26
+ rspec-expectations (~> 2.5.0)
27
+ rspec-mocks (~> 2.5.0)
28
+ rspec-core (2.5.1)
29
+ rspec-expectations (2.5.0)
30
+ diff-lcs (~> 1.1.2)
31
+ rspec-mocks (2.5.0)
32
+ vcr (1.6.0)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ fakeweb
39
+ kanbantastic!
40
+ mocha
41
+ rspec
42
+ vcr
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Jagdeep Singh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File without changes
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run all rspec examples"
7
+ task :spec do
8
+ system "rspec spec"
9
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "kanbantastic/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "kanbantastic"
7
+ s.version = Kanbantastic::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jagdeep Singh"]
10
+ s.email = ["jagdeepkh@gmail.com"]
11
+ s.homepage = "https://github.com/Ennova/Kanbantastic"
12
+ s.summary = %q{Provides a ruby interface to manage your kanbanery resources like a project's tasks, columns and more}
13
+ s.description = %q{Use this gem to interact with kanbanery API}
14
+
15
+ s.rubyforge_project = "kanbantastic"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ s.licenses = ["MIT"]
22
+ s.rubygems_version = %q{1.3.7}
23
+ s.extra_rdoc_files = [
24
+ "LICENSE.txt",
25
+ "README.rdoc"
26
+ ]
27
+
28
+ s.add_runtime_dependency('activemodel')
29
+ s.add_runtime_dependency('httparty')
30
+
31
+ s.add_development_dependency('rspec')
32
+ s.add_development_dependency('fakeweb')
33
+ s.add_development_dependency('vcr')
34
+ s.add_development_dependency('mocha')
35
+ end
@@ -0,0 +1,8 @@
1
+ require 'active_model'
2
+ require 'httparty'
3
+
4
+ require File.join(File.dirname(__FILE__), 'kanbantastic/base')
5
+ require File.join(File.dirname(__FILE__), 'kanbantastic/task')
6
+ require File.join(File.dirname(__FILE__), 'kanbantastic/column')
7
+ require File.join(File.dirname(__FILE__), 'kanbantastic/config')
8
+ require File.join(File.dirname(__FILE__), 'kanbantastic/user')
@@ -0,0 +1,140 @@
1
+ module Kanbantastic
2
+
3
+ class Base
4
+ include HTTParty
5
+ include ActiveModel::Validations
6
+ validates_presence_of :config
7
+
8
+ RECTIFY_TIME_FOR = [:updated_at, :created_at, :moved_at]
9
+
10
+ attr_accessor :config
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ self.valid?
15
+ end
16
+
17
+ def get(url, options={})
18
+ check_parameter_format(:url => [url, String], :options => [options, Hash])
19
+
20
+ self.class.setup_headers(config.api_key)
21
+ response = self.class.get(self.class.base_uri(config.workspace) + url, options)
22
+ if response.code == 200
23
+ self.class.parse_response(response)
24
+ else
25
+ raise response.headers['status']
26
+ end
27
+ end
28
+
29
+ def post(url, options={})
30
+ check_parameter_format(:url => [url, String], :options => [options, Hash])
31
+
32
+ self.class.setup_headers(config.api_key)
33
+ response = self.class.post(self.class.base_uri(config.workspace) + url, options)
34
+ if response.code == 201
35
+ self.class.parse_response(response)
36
+ else
37
+ raise response.headers['status']
38
+ end
39
+ end
40
+
41
+ def put(url, options={})
42
+ check_parameter_format(:url => [url, String], :options => [options, Hash])
43
+
44
+ self.class.setup_headers(config.api_key)
45
+ response = self.class.put(self.class.base_uri(config.workspace) + url, options)
46
+ if response.code == 200
47
+ self.class.parse_response(response)
48
+ else
49
+ raise response.headers['status']
50
+ end
51
+ end
52
+
53
+ def project_id
54
+ config.project_id
55
+ end
56
+
57
+ def assign_attributes(params={})
58
+ params.each do |key, value|
59
+ method_name = key.to_s+"="
60
+ self.send(method_name, value) if self.respond_to?(method_name)
61
+ end
62
+ return self
63
+ end
64
+
65
+ def self.find_project_id(project_name, workspace_name, api_key)
66
+ setup_headers(api_key)
67
+ workspaces = get(base_uri(workspace_name) + "/user/workspaces.json").parsed_response
68
+ if workspace_name
69
+ workspace = workspaces.select{|w| w['name'] == workspace_name }.first
70
+ project = workspace['projects'].select{|p| p['name'] == project_name }.first if workspace
71
+ end
72
+ return project['id'] if project
73
+ end
74
+
75
+ def self.request(method, config, url, options={})
76
+ new(config).send(method.to_s, url, options)
77
+ end
78
+
79
+ def self.invalid_response_error(msg, response)
80
+ begin
81
+ text = response.map{|k,v| v.map{|e| "#{k} #{e}" }.join(', ')}.join(', ')
82
+ rescue
83
+ raise msg
84
+ end
85
+ raise "#{msg} #{text}"
86
+ end
87
+
88
+ private
89
+
90
+ def self.parse_response response
91
+ rectify_time(symbolize_keys(response.parsed_response))
92
+ end
93
+
94
+ def self.rectify_time response
95
+ diff = server_time_difference
96
+ if response.class == Array
97
+ response.each{|r| RECTIFY_TIME_FOR.each{|k| (r[k] += diff) if r[k]}}
98
+ else
99
+ RECTIFY_TIME_FOR.each{|k| (response[k] += diff) if response[k]}
100
+ end
101
+ return response
102
+ end
103
+
104
+ def self.server_time_difference
105
+ diff = (Time.now.utc.to_i - Time.parse(head("https://kanbanery.com/api/v1/test.json").headers['date']).utc.to_i)
106
+ if diff > 1
107
+ raise "Kanbanery server has a time difference of #{diff} seconds"
108
+ else
109
+ return diff
110
+ end
111
+ end
112
+
113
+ def check_parameter_format options={}
114
+ raise "options must be a Hash" unless options.instance_of?(Hash)
115
+ options.each do |key, value|
116
+ raise "options Hash must have each key as a Symbol or a String" unless key.instance_of?(Symbol) or key.instance_of?(String)
117
+ if value.class != Array or value.length != 2 or value[1].class != Class
118
+ raise "each value of options Hash must be an Array of a parameter and its expected class."
119
+ end
120
+ raise "#{key} must be a #{value[1]}." unless value[0].instance_of? value[1]
121
+ end
122
+ end
123
+
124
+ def self.base_uri workspace
125
+ "https://#{workspace}.kanbanery.com/api/v1"
126
+ end
127
+
128
+ def self.setup_headers api_key
129
+ headers['X-Kanbanery-ApiToken'] = api_key
130
+ end
131
+
132
+ def self.symbolize_keys(hsh)
133
+ if hsh.class == Array
134
+ hsh.map{|x| x.inject({}){|h,(k,v)| h[k.to_sym] = v; h}}
135
+ else
136
+ hsh.inject({}){|h,(k,v)| h[k.to_sym] = v; h}
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,45 @@
1
+ module Kanbantastic
2
+
3
+ class Column < Base
4
+ attr_reader :id, :name, :position
5
+ validates_presence_of :id, :name, :position
6
+
7
+ def initialize(config, options={})
8
+ super(config)
9
+ @id = options[:id]
10
+ @name = options[:name]
11
+ @position = options[:position]
12
+ self.valid?
13
+ end
14
+
15
+ def first?
16
+ self.position == 1
17
+ end
18
+
19
+ def second?
20
+ self.position == 2
21
+ end
22
+
23
+ def last?
24
+ Kanbantastic::Column.all(config).last.id == id
25
+ end
26
+
27
+ def ==(column)
28
+ id == column.id
29
+ end
30
+
31
+ def self.find(config, id)
32
+ Kanbantastic::Column.all(config).select{|c| c.id == id }[0]
33
+ end
34
+
35
+ def self.all(config)
36
+ collection = []
37
+ response = request(:get, config, "/projects/#{config.project_id}/columns.json")
38
+ response.each do |data|
39
+ column = new(config, data)
40
+ collection << column if column.valid?
41
+ end
42
+ return collection
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ module Kanbantastic
2
+
3
+ class Config
4
+ include ActiveModel::Validations
5
+ attr_reader :api_key, :workspace, :project_id
6
+ validates_presence_of :api_key, :workspace, :project_id
7
+
8
+ def initialize(api_key, workspace, project_id)
9
+ @api_key = api_key
10
+ @workspace = workspace
11
+ @project_id = project_id
12
+ self.valid?
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,130 @@
1
+ module Kanbantastic
2
+
3
+ class Task < Base
4
+ attr_accessor :id, :title, :column_id, :updated_at, :created_at, :moved_at, :task_type_id, :owner_id, :position
5
+ validates_presence_of :config, :id, :title, :column_id, :updated_at, :task_type_id
6
+
7
+ def initialize(config, options = {})
8
+ super(config)
9
+ assign_attributes(options)
10
+ @task_type_id = options[:task_type_id] || find_task_type_id(options[:task_type_name])
11
+ valid?
12
+ end
13
+
14
+ def find_task_type_id(task_type_name)
15
+ @task_type_ids ||= {}
16
+ @task_type_ids[task_type_name] ||= if !task_type_name.blank?
17
+ response = get("/projects/#{project_id}/task_types.json")
18
+ task_type = response.select{|t| t[:name] == task_type_name}.first || Hash.new
19
+ task_type[:id]
20
+ end
21
+ end
22
+
23
+ def update(params={})
24
+ params[:owner_id] ||= self.owner_id
25
+ begin
26
+ response = put("/tasks/#{id}.json", :body => {:task => params})
27
+ rescue Exception => e
28
+ self.class.invalid_response_error("Unable to update task. #{e.message}", response)
29
+ end
30
+ if response[:id]
31
+ assign_attributes(response)
32
+ return valid?
33
+ else
34
+ self.class.invalid_response_error("Unable to update task.", response)
35
+ end
36
+ end
37
+
38
+ def self.create(config, params={})
39
+ # raise exception if title and task_type_name is not present
40
+ unless params[:title] and params[:task_type_name]
41
+ raise "Kanbanery task can't be created without title and task_type_name."
42
+ end
43
+ response = request(:post, config, "/projects/#{config.project_id}/tasks.json", :body => {:task => params})
44
+ task = new(config, response)
45
+ task.valid? ? task : invalid_response_error("Unable to create task.", response)
46
+ end
47
+
48
+ def column
49
+ @columns ||= {}
50
+ @columns[column_id] ||= Kanbantastic::Column.find(config, column_id)
51
+ end
52
+
53
+ def move_to_next_column
54
+ update(:location => 'next_column')
55
+ end
56
+
57
+ def move_to_previous_column
58
+ update(:location => 'prev_column')
59
+ end
60
+
61
+ def move_to_first_column
62
+ column = Kanbantastic::Column.all(config).first
63
+ update(:column_id => column.id, :position => nil)
64
+ end
65
+
66
+ def move_to_second_column
67
+ column = Kanbantastic::Column.all(config)[1]
68
+ update(:column_id => column.id, :position => nil)
69
+ end
70
+
71
+ def move_to_last_column
72
+ column = Kanbantastic::Column.all(config).last
73
+ update(:column_id => column.id, :position => nil)
74
+ end
75
+
76
+ def archive
77
+ if Kanbantastic::Task.archived?(config, id)
78
+ return true
79
+ else
80
+ # raise an exception as only tasks which are in last column can be archived.
81
+ unless column.last?
82
+ raise "Kanbanery tasks can be archived only from the last column."
83
+ end
84
+ update(:location => "archive")
85
+ end
86
+ end
87
+
88
+ def owner
89
+ @owner ||= if owner_id
90
+ response = get("/projects/#{project_id}/users.json")
91
+ user = response.select{|r| r[:id] == owner_id}[0]
92
+ Kanbantastic::User.new("#{user[:first_name]} #{user[:last_name]}", user[:email], user[:gravatar_url])
93
+ end
94
+ end
95
+
96
+ def ==(task)
97
+ id == task.id
98
+ end
99
+
100
+ def self.archived?(config, id)
101
+ response = request(:get, config, "/projects/#{config.project_id}/archive/tasks.json")
102
+ archived_task = response.select{|t| t[:id] == id}[0]
103
+ archived_task ? true : false
104
+ end
105
+
106
+ def self.find(config, id)
107
+ begin
108
+ response = request(:get, config, "/tasks/#{id}.json")
109
+ rescue
110
+ return nil
111
+ end
112
+ task = new(config, response)
113
+ if task.valid?
114
+ return task
115
+ else
116
+ return nil
117
+ end
118
+ end
119
+
120
+ def self.all(config)
121
+ collection = []
122
+ response = request(:get, config, "/projects/#{config.project_id}/tasks.json")
123
+ response.each do |data|
124
+ task = self.new(config, data)
125
+ collection << task if task.valid?
126
+ end
127
+ return collection
128
+ end
129
+ end
130
+ end