kanbantastic 0.1.5

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